mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-29 15:15:27 -07:00
Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83906c6c4b | ||
|
|
65f132e9c2 | ||
|
|
c211e52382 | ||
|
|
7c093130c6 | ||
|
|
8ffd5bc008 | ||
|
|
2542f64e1b | ||
|
|
9bde534860 | ||
|
|
97eb570b28 | ||
|
|
7d0557a099 | ||
|
|
20851ccc2b | ||
|
|
e5f4606a6c | ||
|
|
146ef5aaeb | ||
|
|
971604d14f | ||
|
|
7fcca09f24 |
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 0.1.11
|
current_version = 0.1.12
|
||||||
commit = True
|
commit = True
|
||||||
tag = True
|
tag = True
|
||||||
tag_name = v{new_version}
|
tag_name = v{new_version}
|
||||||
|
|||||||
@@ -66,24 +66,24 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
pip install -r backend/requirements-mlx.txt
|
pip install -r backend/requirements-mlx.txt
|
||||||
|
|
||||||
- name: Install PyTorch with CUDA (Windows only)
|
|
||||||
if: matrix.platform == 'windows-latest'
|
|
||||||
run: |
|
|
||||||
pip install torch --index-url https://download.pytorch.org/whl/cu121 --force-reinstall --no-deps
|
|
||||||
pip install torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
|
|
||||||
|
|
||||||
- name: Build Python server (Linux/macOS)
|
- name: Build Python server (Linux/macOS)
|
||||||
if: matrix.platform != 'windows-latest'
|
if: matrix.platform != 'windows-latest'
|
||||||
run: |
|
run: |
|
||||||
chmod +x scripts/build-server.sh
|
chmod +x scripts/build-server.sh
|
||||||
./scripts/build-server.sh
|
./scripts/build-server.sh
|
||||||
|
|
||||||
- name: Build Python server (Windows)
|
- name: Build CPU Python server (Windows)
|
||||||
if: matrix.platform == 'windows-latest'
|
if: matrix.platform == 'windows-latest'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
cd backend
|
cd backend
|
||||||
python build_binary.py
|
|
||||||
|
echo "Installing CPU-only PyTorch..."
|
||||||
|
pip uninstall -y torch torchvision torchaudio
|
||||||
|
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
|
||||||
|
|
||||||
|
echo "Building CPU server binary..."
|
||||||
|
python build_binary.py cpu
|
||||||
|
|
||||||
# Get platform tuple
|
# Get platform tuple
|
||||||
PLATFORM=$(rustc --print host-tuple)
|
PLATFORM=$(rustc --print host-tuple)
|
||||||
@@ -91,9 +91,31 @@ jobs:
|
|||||||
# Create binaries directory
|
# Create binaries directory
|
||||||
mkdir -p ../tauri/src-tauri/binaries
|
mkdir -p ../tauri/src-tauri/binaries
|
||||||
|
|
||||||
# Copy with platform suffix
|
# Copy CPU version (default for installer)
|
||||||
cp dist/voicebox-server.exe ../tauri/src-tauri/binaries/voicebox-server-${PLATFORM}.exe
|
cp dist/voicebox-server.exe ../tauri/src-tauri/binaries/voicebox-server-${PLATFORM}.exe
|
||||||
echo "Built voicebox-server-${PLATFORM}.exe"
|
echo "Built CPU server: voicebox-server-${PLATFORM}.exe (~500MB)"
|
||||||
|
|
||||||
|
- name: Build CUDA Python server (Windows)
|
||||||
|
if: matrix.platform == 'windows-latest'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cd backend
|
||||||
|
|
||||||
|
echo "Installing CUDA PyTorch..."
|
||||||
|
pip uninstall -y torch torchvision torchaudio
|
||||||
|
pip install torch --index-url https://download.pytorch.org/whl/cu121 --force-reinstall --no-deps
|
||||||
|
pip install torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
|
||||||
|
|
||||||
|
echo "Building CUDA server binary..."
|
||||||
|
python build_binary.py cuda
|
||||||
|
|
||||||
|
# Get platform tuple
|
||||||
|
PLATFORM=$(rustc --print host-tuple)
|
||||||
|
|
||||||
|
# Copy CUDA version for separate upload
|
||||||
|
mkdir -p cuda-release
|
||||||
|
cp dist/voicebox-server-cuda.exe cuda-release/voicebox-server-cuda-${PLATFORM}.exe
|
||||||
|
echo "Built CUDA server: voicebox-server-cuda-${PLATFORM}.exe (~3GB)"
|
||||||
|
|
||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: oven-sh/setup-bun@v2
|
uses: oven-sh/setup-bun@v2
|
||||||
@@ -150,11 +172,41 @@ jobs:
|
|||||||
### Installation
|
### Installation
|
||||||
- **macOS (Apple Silicon)**: Download the `aarch64.dmg` file - uses MLX for fast native inference
|
- **macOS (Apple Silicon)**: Download the `aarch64.dmg` file - uses MLX for fast native inference
|
||||||
- **macOS (Intel)**: Download the `x64.dmg` file - uses PyTorch
|
- **macOS (Intel)**: Download the `x64.dmg` file - uses PyTorch
|
||||||
- **Windows**: Download the `.msi` installer
|
- **Windows**: Download the `.msi` installer - includes CPU-only inference (~500MB)
|
||||||
- **Linux**: Download the `.AppImage` or `.deb` package
|
- **Linux**: Download the `.AppImage` or `.deb` package
|
||||||
|
|
||||||
|
### NVIDIA GPU Acceleration (Windows)
|
||||||
|
Windows users with NVIDIA GPUs can enable CUDA for 4-5x faster inference:
|
||||||
|
1. Install the app normally (CPU version included in installer)
|
||||||
|
2. The app will detect your GPU and offer to download CUDA support automatically
|
||||||
|
3. Or manually download: [voicebox-server-cuda-x86_64-pc-windows-msvc.exe](https://downloads.voicebox.sh/cuda/__VERSION__/voicebox-server-cuda-x86_64-pc-windows-msvc.exe) (~2.4GB)
|
||||||
|
|
||||||
The app includes automatic updates - future updates will be installed automatically.
|
The app includes automatic updates - future updates will be installed automatically.
|
||||||
releaseDraft: true
|
releaseDraft: true
|
||||||
prerelease: false
|
prerelease: false
|
||||||
args: ${{ matrix.args }}
|
args: ${{ matrix.args }}
|
||||||
includeUpdaterJson: true
|
includeUpdaterJson: true
|
||||||
|
|
||||||
|
- name: Upload CUDA server to Cloudflare R2 (Windows only)
|
||||||
|
if: matrix.platform == 'windows-latest'
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||||
|
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||||
|
run: |
|
||||||
|
# Install AWS CLI if not available
|
||||||
|
pip install awscli
|
||||||
|
|
||||||
|
# Get version from tag
|
||||||
|
VERSION=${GITHUB_REF#refs/tags/}
|
||||||
|
|
||||||
|
# Get platform tuple
|
||||||
|
PLATFORM=$(rustc --print host-tuple)
|
||||||
|
|
||||||
|
# Upload to R2
|
||||||
|
aws s3 cp backend/cuda-release/voicebox-server-cuda-${PLATFORM}.exe \
|
||||||
|
s3://voicebox/cuda/${VERSION}/voicebox-server-cuda-${PLATFORM}.exe \
|
||||||
|
--endpoint-url $R2_ENDPOINT \
|
||||||
|
--acl public-read
|
||||||
|
|
||||||
|
echo "CUDA binary uploaded to: https://downloads.voicebox.sh/cuda/${VERSION}/voicebox-server-cuda-${PLATFORM}.exe"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@voicebox/app",
|
"name": "@voicebox/app",
|
||||||
"version": "0.1.11",
|
"version": "0.1.12",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ export function HistoryTable() {
|
|||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [importDialogOpen, setImportDialogOpen] = useState(false);
|
const [importDialogOpen, setImportDialogOpen] = useState(false);
|
||||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||||
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
|
const [generationToDelete, setGenerationToDelete] = useState<{ id: string; name: string } | null>(null);
|
||||||
const limit = 20;
|
const limit = 20;
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
@@ -179,6 +181,19 @@ export function HistoryTable() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDeleteClick = (generationId: string, profileName: string) => {
|
||||||
|
setGenerationToDelete({ id: generationId, name: profileName });
|
||||||
|
setDeleteDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteConfirm = () => {
|
||||||
|
if (generationToDelete) {
|
||||||
|
deleteGeneration.mutate(generationToDelete.id);
|
||||||
|
setDeleteDialogOpen(false);
|
||||||
|
setGenerationToDelete(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleImportConfirm = () => {
|
const handleImportConfirm = () => {
|
||||||
if (selectedFile) {
|
if (selectedFile) {
|
||||||
importGeneration.mutate(selectedFile, {
|
importGeneration.mutate(selectedFile, {
|
||||||
@@ -320,7 +335,7 @@ export function HistoryTable() {
|
|||||||
Export Package
|
Export Package
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => deleteGeneration.mutate(gen.id)}
|
onClick={() => handleDeleteClick(gen.id, gen.profile_name)}
|
||||||
disabled={deleteGeneration.isPending}
|
disabled={deleteGeneration.isPending}
|
||||||
className="text-destructive focus:text-destructive"
|
className="text-destructive focus:text-destructive"
|
||||||
>
|
>
|
||||||
@@ -351,6 +366,35 @@ export function HistoryTable() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Delete Generation</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Are you sure you want to delete this generation from "{generationToDelete?.name}"? This action cannot be undone.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setDeleteDialogOpen(false);
|
||||||
|
setGenerationToDelete(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={handleDeleteConfirm}
|
||||||
|
disabled={deleteGeneration.isPending}
|
||||||
|
>
|
||||||
|
{deleteGeneration.isPending ? 'Deleting...' : 'Delete'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<Dialog open={importDialogOpen} onOpenChange={setImportDialogOpen}>
|
<Dialog open={importDialogOpen} onOpenChange={setImportDialogOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
|
|||||||
@@ -2,6 +2,14 @@ import { Check, Edit, Pause, Play, Plus, Trash2, Volume2, X } from 'lucide-react
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { CircleButton } from '@/components/ui/circle-button';
|
import { CircleButton } from '@/components/ui/circle-button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
import { Slider } from '@/components/ui/slider';
|
import { Slider } from '@/components/ui/slider';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { useToast } from '@/components/ui/use-toast';
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
@@ -140,10 +148,19 @@ export function SampleList({ profileId }: SampleListProps) {
|
|||||||
const [uploadOpen, setUploadOpen] = useState(false);
|
const [uploadOpen, setUploadOpen] = useState(false);
|
||||||
const [editingSampleId, setEditingSampleId] = useState<string | null>(null);
|
const [editingSampleId, setEditingSampleId] = useState<string | null>(null);
|
||||||
const [editedText, setEditedText] = useState<string>('');
|
const [editedText, setEditedText] = useState<string>('');
|
||||||
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
|
const [sampleToDelete, setSampleToDelete] = useState<string | null>(null);
|
||||||
|
|
||||||
const handleDelete = (sampleId: string) => {
|
const handleDeleteClick = (sampleId: string) => {
|
||||||
if (confirm('Are you sure you want to delete this sample?')) {
|
setSampleToDelete(sampleId);
|
||||||
deleteSample.mutate(sampleId);
|
setDeleteDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteConfirm = () => {
|
||||||
|
if (sampleToDelete) {
|
||||||
|
deleteSample.mutate(sampleToDelete);
|
||||||
|
setDeleteDialogOpen(false);
|
||||||
|
setSampleToDelete(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -268,7 +285,7 @@ export function SampleList({ profileId }: SampleListProps) {
|
|||||||
<CircleButton
|
<CircleButton
|
||||||
icon={Trash2}
|
icon={Trash2}
|
||||||
title="Delete sample"
|
title="Delete sample"
|
||||||
onClick={() => handleDelete(sample.id)}
|
onClick={() => handleDeleteClick(sample.id)}
|
||||||
disabled={deleteSample.isPending}
|
disabled={deleteSample.isPending}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -306,6 +323,35 @@ export function SampleList({ profileId }: SampleListProps) {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<SampleUpload profileId={profileId} open={uploadOpen} onOpenChange={setUploadOpen} />
|
<SampleUpload profileId={profileId} open={uploadOpen} onOpenChange={setUploadOpen} />
|
||||||
|
|
||||||
|
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Delete Sample</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Are you sure you want to delete this audio sample? This action cannot be undone.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setDeleteDialogOpen(false);
|
||||||
|
setSampleToDelete(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={handleDeleteConfirm}
|
||||||
|
disabled={deleteSample.isPending}
|
||||||
|
>
|
||||||
|
{deleteSample.isPending ? 'Deleting...' : 'Delete'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ export interface CircleButtonProps extends React.ButtonHTMLAttributes<HTMLButton
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CircleButton = React.forwardRef<HTMLButtonElement, CircleButtonProps>(
|
const CircleButton = React.forwardRef<HTMLButtonElement, CircleButtonProps>(
|
||||||
({ className, icon: Icon, ...props }, ref) => {
|
({ className, icon: Icon, type = 'button', ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
type={type}
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-7 w-7 rounded-full flex items-center justify-center flex-shrink-0',
|
'h-7 w-7 rounded-full flex items-center justify-center flex-shrink-0',
|
||||||
'hover:bg-muted transition-colors',
|
'hover:bg-muted transition-colors',
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
# Backend package
|
# Backend package
|
||||||
|
|
||||||
__version__ = "0.1.11"
|
__version__ = "0.1.12"
|
||||||
|
|||||||
+24
-6
@@ -5,6 +5,7 @@ PyInstaller build script for creating standalone Python server binary.
|
|||||||
import PyInstaller.__main__
|
import PyInstaller.__main__
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
@@ -13,15 +14,27 @@ def is_apple_silicon():
|
|||||||
return platform.system() == "Darwin" and platform.machine() == "arm64"
|
return platform.system() == "Darwin" and platform.machine() == "arm64"
|
||||||
|
|
||||||
|
|
||||||
def build_server():
|
def build_server(variant="cpu"):
|
||||||
"""Build Python server as standalone binary."""
|
"""Build Python server as standalone binary.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
variant: 'cpu' for CPU-only build (~500MB) or 'cuda' for CUDA build (~3GB)
|
||||||
|
"""
|
||||||
backend_dir = Path(__file__).parent
|
backend_dir = Path(__file__).parent
|
||||||
|
|
||||||
|
if variant not in ['cpu', 'cuda']:
|
||||||
|
raise ValueError(f"Invalid variant: {variant}. Must be 'cpu' or 'cuda'")
|
||||||
|
|
||||||
|
# Set binary name based on variant
|
||||||
|
binary_name = f'voicebox-server-{variant}' if variant == 'cuda' else 'voicebox-server'
|
||||||
|
|
||||||
|
print(f"Building {variant.upper()} variant: {binary_name}")
|
||||||
|
|
||||||
# PyInstaller arguments
|
# PyInstaller arguments
|
||||||
args = [
|
args = [
|
||||||
'server.py', # Use server.py as entry point instead of main.py
|
'server.py', # Use server.py as entry point instead of main.py
|
||||||
'--onefile',
|
'--onefile',
|
||||||
'--name', 'voicebox-server',
|
'--name', binary_name,
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add local qwen_tts path if specified (for editable installs)
|
# Add local qwen_tts path if specified (for editable installs)
|
||||||
@@ -100,9 +113,14 @@ def build_server():
|
|||||||
|
|
||||||
# Run PyInstaller
|
# Run PyInstaller
|
||||||
PyInstaller.__main__.run(args)
|
PyInstaller.__main__.run(args)
|
||||||
|
|
||||||
print(f"Binary built in {backend_dir / 'dist' / 'voicebox-server'}")
|
print(f"\n{'='*60}")
|
||||||
|
print(f"Build complete: {variant.upper()} variant")
|
||||||
|
print(f"Binary: {backend_dir / 'dist' / binary_name}")
|
||||||
|
print(f"{'='*60}\n")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
build_server()
|
# Accept variant as command line argument
|
||||||
|
variant = sys.argv[1] if len(sys.argv) > 1 else 'cpu'
|
||||||
|
build_server(variant)
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
@echo off
|
||||||
|
REM Build both CPU and CUDA server binaries for Windows
|
||||||
|
|
||||||
|
echo ============================================================
|
||||||
|
echo Building BOTH server binaries (CPU + CUDA)
|
||||||
|
echo This will take a while...
|
||||||
|
echo ============================================================
|
||||||
|
|
||||||
|
call build_cpu.bat
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo CPU build failed!
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo.
|
||||||
|
|
||||||
|
call build_cuda.bat
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo CUDA build failed!
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ============================================================
|
||||||
|
echo Both binaries built successfully!
|
||||||
|
echo ============================================================
|
||||||
|
echo CPU binary: dist\voicebox-server.exe (~500MB)
|
||||||
|
echo CUDA binary: dist\voicebox-server-cuda.exe (~3GB)
|
||||||
|
echo ============================================================
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
@echo off
|
||||||
|
REM Build CPU-only server binary for Windows
|
||||||
|
REM This creates a ~500MB binary without CUDA support
|
||||||
|
|
||||||
|
echo ============================================================
|
||||||
|
echo Building CPU-only server binary
|
||||||
|
echo ============================================================
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Step 1: Installing CPU-only PyTorch...
|
||||||
|
pip uninstall -y torch torchvision torchaudio
|
||||||
|
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Step 2: Building binary with PyInstaller...
|
||||||
|
python build_binary.py cpu
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Step 3: Restoring CUDA PyTorch for development...
|
||||||
|
pip uninstall -y torch torchvision torchaudio
|
||||||
|
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ============================================================
|
||||||
|
echo CPU binary built successfully!
|
||||||
|
echo Location: dist\voicebox-server.exe
|
||||||
|
echo Size: ~500MB
|
||||||
|
echo ============================================================
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Build CPU-only server binary
|
||||||
|
# This creates a ~500MB binary without CUDA support
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "============================================================"
|
||||||
|
echo "Building CPU-only server binary"
|
||||||
|
echo "============================================================"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Step 1: Installing CPU-only PyTorch..."
|
||||||
|
pip uninstall -y torch torchvision torchaudio || true
|
||||||
|
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Step 2: Building binary with PyInstaller..."
|
||||||
|
python build_binary.py cpu
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Step 3: Restoring CUDA PyTorch for development..."
|
||||||
|
pip uninstall -y torch torchvision torchaudio || true
|
||||||
|
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "============================================================"
|
||||||
|
echo "CPU binary built successfully!"
|
||||||
|
echo "Location: dist/voicebox-server"
|
||||||
|
echo "Size: ~500MB"
|
||||||
|
echo "============================================================"
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
@echo off
|
||||||
|
REM Build CUDA server binary for Windows
|
||||||
|
REM This creates a ~3GB binary with CUDA support
|
||||||
|
|
||||||
|
echo ============================================================
|
||||||
|
echo Building CUDA server binary
|
||||||
|
echo ============================================================
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Step 1: Ensuring CUDA PyTorch is installed...
|
||||||
|
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 --upgrade
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Step 2: Building binary with PyInstaller...
|
||||||
|
python build_binary.py cuda
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ============================================================
|
||||||
|
echo CUDA binary built successfully!
|
||||||
|
echo Location: dist\voicebox-server-cuda.exe
|
||||||
|
echo Size: ~3GB
|
||||||
|
echo ============================================================
|
||||||
+22
-1
@@ -22,7 +22,7 @@ from .database import (
|
|||||||
)
|
)
|
||||||
from .utils.audio import validate_reference_audio, load_audio, save_audio
|
from .utils.audio import validate_reference_audio, load_audio, save_audio
|
||||||
from .utils.images import validate_image, process_avatar
|
from .utils.images import validate_image, process_avatar
|
||||||
from .utils.cache import _get_cache_dir
|
from .utils.cache import _get_cache_dir, clear_profile_cache
|
||||||
from .tts import get_tts_model
|
from .tts import get_tts_model
|
||||||
from . import config
|
from . import config
|
||||||
|
|
||||||
@@ -121,6 +121,10 @@ async def add_profile_sample(
|
|||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(db_sample)
|
db.refresh(db_sample)
|
||||||
|
|
||||||
|
# Invalidate combined audio cache for this profile
|
||||||
|
# Since a new sample was added, any cached combined audio is now stale
|
||||||
|
clear_profile_cache(profile_id)
|
||||||
|
|
||||||
return ProfileSampleResponse.model_validate(db_sample)
|
return ProfileSampleResponse.model_validate(db_sample)
|
||||||
|
|
||||||
|
|
||||||
@@ -242,6 +246,9 @@ async def delete_profile(
|
|||||||
if profile_dir.exists():
|
if profile_dir.exists():
|
||||||
shutil.rmtree(profile_dir)
|
shutil.rmtree(profile_dir)
|
||||||
|
|
||||||
|
# Clean up combined audio cache files for this profile
|
||||||
|
clear_profile_cache(profile_id)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -263,6 +270,9 @@ async def delete_profile_sample(
|
|||||||
if not sample:
|
if not sample:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Store profile_id before deleting
|
||||||
|
profile_id = sample.profile_id
|
||||||
|
|
||||||
# Delete audio file
|
# Delete audio file
|
||||||
audio_path = Path(sample.audio_path)
|
audio_path = Path(sample.audio_path)
|
||||||
if audio_path.exists():
|
if audio_path.exists():
|
||||||
@@ -272,6 +282,10 @@ async def delete_profile_sample(
|
|||||||
db.delete(sample)
|
db.delete(sample)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
# Invalidate combined audio cache for this profile
|
||||||
|
# Since the sample set changed, any cached combined audio is now stale
|
||||||
|
clear_profile_cache(profile_id)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -295,10 +309,17 @@ async def update_profile_sample(
|
|||||||
if not sample:
|
if not sample:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Store profile_id before updating
|
||||||
|
profile_id = sample.profile_id
|
||||||
|
|
||||||
sample.reference_text = reference_text
|
sample.reference_text = reference_text
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(sample)
|
db.refresh(sample)
|
||||||
|
|
||||||
|
# Invalidate combined audio cache for this profile
|
||||||
|
# Since the reference text changed, cache keys and combined text are now stale
|
||||||
|
clear_profile_cache(profile_id)
|
||||||
|
|
||||||
return ProfileSampleResponse.model_validate(sample)
|
return ProfileSampleResponse.model_validate(sample)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test CUDA binary compression to verify it fits under GitHub's 2GB release asset limit.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python test_cuda_compression.py [path/to/voicebox-server-cuda.exe]
|
||||||
|
|
||||||
|
If no path provided, looks for the binary in ./dist/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def format_size(bytes_size):
|
||||||
|
"""Format bytes into human-readable size."""
|
||||||
|
for unit in ['B', 'KB', 'MB', 'GB']:
|
||||||
|
if bytes_size < 1024.0:
|
||||||
|
return f"{bytes_size:.2f} {unit}"
|
||||||
|
bytes_size /= 1024.0
|
||||||
|
return f"{bytes_size:.2f} TB"
|
||||||
|
|
||||||
|
|
||||||
|
def get_file_size(filepath):
|
||||||
|
"""Get file size in bytes."""
|
||||||
|
return os.path.getsize(filepath)
|
||||||
|
|
||||||
|
|
||||||
|
def compress_with_7z(input_file, output_file):
|
||||||
|
"""Compress file using 7z with maximum compression."""
|
||||||
|
print(f"\nCompressing with 7z (maximum compression)...")
|
||||||
|
print(f"This may take several minutes for a ~2.5GB file...\n")
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
'7z', 'a',
|
||||||
|
'-t7z', # 7z format
|
||||||
|
'-m0=lzma2', # LZMA2 compression
|
||||||
|
'-mx=9', # Maximum compression
|
||||||
|
'-mfb=64', # Fast bytes
|
||||||
|
'-md=32m', # Dictionary size
|
||||||
|
'-ms=on', # Solid archive
|
||||||
|
output_file,
|
||||||
|
input_file
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"Error during compression: {e}")
|
||||||
|
print(f"stderr: {e.stderr}")
|
||||||
|
return False
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("ERROR: 7z not found. Please install 7-Zip:")
|
||||||
|
print(" Windows: https://www.7-zip.org/download.html")
|
||||||
|
print(" macOS: brew install p7zip")
|
||||||
|
print(" Linux: apt-get install p7zip-full")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Find CUDA binary
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
cuda_binary = Path(sys.argv[1])
|
||||||
|
else:
|
||||||
|
# Look in dist directory
|
||||||
|
dist_dir = Path(__file__).parent / 'dist'
|
||||||
|
candidates = list(dist_dir.glob('voicebox-server-cuda*.exe'))
|
||||||
|
|
||||||
|
if not candidates:
|
||||||
|
print("ERROR: CUDA binary not found in ./dist/")
|
||||||
|
print("Please provide the path as an argument:")
|
||||||
|
print(" python test_cuda_compression.py path/to/voicebox-server-cuda.exe")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
cuda_binary = candidates[0]
|
||||||
|
|
||||||
|
if not cuda_binary.exists():
|
||||||
|
print(f"ERROR: File not found: {cuda_binary}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("=" * 70)
|
||||||
|
print("CUDA Binary Compression Test")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
# Get original size
|
||||||
|
original_size = get_file_size(cuda_binary)
|
||||||
|
print(f"\nOriginal file: {cuda_binary.name}")
|
||||||
|
print(f"Original size: {format_size(original_size)} ({original_size:,} bytes)")
|
||||||
|
|
||||||
|
# Check if already over 2GB
|
||||||
|
github_limit = 2 * 1024 * 1024 * 1024 # 2GB in bytes
|
||||||
|
print(f"GitHub limit: {format_size(github_limit)} ({github_limit:,} bytes)")
|
||||||
|
|
||||||
|
if original_size > github_limit:
|
||||||
|
print(f"\n[WARNING] Original file exceeds GitHub limit by {format_size(original_size - github_limit)}")
|
||||||
|
else:
|
||||||
|
print(f"\n[OK] Original file is under GitHub limit")
|
||||||
|
|
||||||
|
# Compress
|
||||||
|
output_file = cuda_binary.parent / f"{cuda_binary.stem}.7z"
|
||||||
|
if output_file.exists():
|
||||||
|
print(f"\nRemoving existing compressed file: {output_file.name}")
|
||||||
|
output_file.unlink()
|
||||||
|
|
||||||
|
success = compress_with_7z(cuda_binary, output_file)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Check compressed size
|
||||||
|
compressed_size = get_file_size(output_file)
|
||||||
|
compression_ratio = (1 - compressed_size / original_size) * 100
|
||||||
|
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print("Compression Results")
|
||||||
|
print("=" * 70)
|
||||||
|
print(f"\nCompressed file: {output_file.name}")
|
||||||
|
print(f"Compressed size: {format_size(compressed_size)} ({compressed_size:,} bytes)")
|
||||||
|
print(f"Compression ratio: {compression_ratio:.1f}%")
|
||||||
|
print(f"Space saved: {format_size(original_size - compressed_size)}")
|
||||||
|
|
||||||
|
if compressed_size <= github_limit:
|
||||||
|
print(f"\n[SUCCESS] Compressed file fits under GitHub's 2GB limit!")
|
||||||
|
print(f" Margin: {format_size(github_limit - compressed_size)} remaining")
|
||||||
|
else:
|
||||||
|
print(f"\n[FAILED] Compressed file still exceeds GitHub limit")
|
||||||
|
print(f" Over by: {format_size(compressed_size - github_limit)}")
|
||||||
|
print(f"\n Alternative: Host on external storage (S3, Azure Blob, etc.)")
|
||||||
|
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Test R2 upload locally before running in CI
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "============================================================"
|
||||||
|
echo "Cloudflare R2 Upload Test"
|
||||||
|
echo "============================================================"
|
||||||
|
|
||||||
|
# Check for required environment variables
|
||||||
|
if [ -z "$AWS_ACCESS_KEY_ID" ] || [ -z "$AWS_SECRET_ACCESS_KEY" ] || [ -z "$R2_ENDPOINT" ]; then
|
||||||
|
echo "ERROR: Missing required environment variables"
|
||||||
|
echo ""
|
||||||
|
echo "Please set:"
|
||||||
|
echo " export AWS_ACCESS_KEY_ID='your-r2-access-key-id'"
|
||||||
|
echo " export AWS_SECRET_ACCESS_KEY='your-r2-secret-access-key'"
|
||||||
|
echo " export R2_ENDPOINT='https://your-account-id.r2.cloudflarestorage.com'"
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for AWS CLI
|
||||||
|
if ! command -v aws &> /dev/null; then
|
||||||
|
echo "Installing AWS CLI..."
|
||||||
|
pip install awscli
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find CUDA binary
|
||||||
|
CUDA_BINARY=$(ls dist/voicebox-server-cuda*.exe 2>/dev/null | head -1)
|
||||||
|
|
||||||
|
if [ -z "$CUDA_BINARY" ]; then
|
||||||
|
echo "ERROR: CUDA binary not found in dist/"
|
||||||
|
echo "Run: bash build_cuda.bat"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Found CUDA binary: $CUDA_BINARY"
|
||||||
|
echo "Size: $(du -h "$CUDA_BINARY" | cut -f1)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test version
|
||||||
|
VERSION="v0.1.12-test"
|
||||||
|
PLATFORM="x86_64-pc-windows-msvc"
|
||||||
|
FILENAME="voicebox-server-cuda-${PLATFORM}.exe"
|
||||||
|
|
||||||
|
echo "Test upload configuration:"
|
||||||
|
echo " Version: $VERSION"
|
||||||
|
echo " Platform: $PLATFORM"
|
||||||
|
echo " Endpoint: $R2_ENDPOINT"
|
||||||
|
echo " Bucket: voicebox"
|
||||||
|
echo " Path: cuda/$VERSION/$FILENAME"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
read -p "Proceed with upload? (y/n) " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo "Aborted."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Uploading to R2..."
|
||||||
|
|
||||||
|
aws s3 cp "$CUDA_BINARY" \
|
||||||
|
"s3://voicebox/cuda/${VERSION}/${FILENAME}" \
|
||||||
|
--endpoint-url "$R2_ENDPOINT" \
|
||||||
|
--acl public-read
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "============================================================"
|
||||||
|
echo "Upload successful!"
|
||||||
|
echo "============================================================"
|
||||||
|
echo ""
|
||||||
|
echo "Download URL:"
|
||||||
|
echo "https://downloads.voicebox.sh/cuda/${VERSION}/${FILENAME}"
|
||||||
|
echo ""
|
||||||
|
echo "Test with:"
|
||||||
|
echo "curl -I https://downloads.voicebox.sh/cuda/${VERSION}/${FILENAME}"
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo "Upload failed!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -105,11 +105,46 @@ def clear_voice_prompt_cache() -> int:
|
|||||||
deleted_count = 0
|
deleted_count = 0
|
||||||
|
|
||||||
if cache_dir.exists():
|
if cache_dir.exists():
|
||||||
|
# Delete prompt cache files
|
||||||
for cache_file in cache_dir.glob("*.prompt"):
|
for cache_file in cache_dir.glob("*.prompt"):
|
||||||
try:
|
try:
|
||||||
cache_file.unlink()
|
cache_file.unlink()
|
||||||
deleted_count += 1
|
deleted_count += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed to delete cache file {cache_file}: {e}")
|
print(f"Failed to delete cache file {cache_file}: {e}")
|
||||||
|
|
||||||
|
# Delete combined audio files
|
||||||
|
for audio_file in cache_dir.glob("combined_*.wav"):
|
||||||
|
try:
|
||||||
|
audio_file.unlink()
|
||||||
|
deleted_count += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to delete combined audio file {audio_file}: {e}")
|
||||||
|
|
||||||
|
return deleted_count
|
||||||
|
|
||||||
|
|
||||||
|
def clear_profile_cache(profile_id: str) -> int:
|
||||||
|
"""
|
||||||
|
Clear cache files for a specific profile.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
profile_id: Profile ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of cache files deleted
|
||||||
|
"""
|
||||||
|
cache_dir = _get_cache_dir()
|
||||||
|
deleted_count = 0
|
||||||
|
|
||||||
|
if cache_dir.exists():
|
||||||
|
# Delete combined audio files for this profile
|
||||||
|
pattern = f"combined_{profile_id}_*.wav"
|
||||||
|
for audio_file in cache_dir.glob(pattern):
|
||||||
|
try:
|
||||||
|
audio_file.unlink()
|
||||||
|
deleted_count += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to delete combined audio file {audio_file}: {e}")
|
||||||
|
|
||||||
return deleted_count
|
return deleted_count
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
from PyInstaller.utils.hooks import collect_data_files
|
||||||
|
from PyInstaller.utils.hooks import collect_submodules
|
||||||
|
from PyInstaller.utils.hooks import copy_metadata
|
||||||
|
|
||||||
|
datas = []
|
||||||
|
hiddenimports = ['backend', 'backend.main', 'backend.config', 'backend.database', 'backend.models', 'backend.profiles', 'backend.history', 'backend.tts', 'backend.transcribe', 'backend.platform_detect', 'backend.backends', 'backend.backends.pytorch_backend', 'backend.utils.audio', 'backend.utils.cache', 'backend.utils.progress', 'backend.utils.hf_progress', 'backend.utils.validation', 'torch', 'transformers', 'fastapi', 'uvicorn', 'sqlalchemy', 'librosa', 'soundfile', 'qwen_tts', 'qwen_tts.inference', 'qwen_tts.inference.qwen3_tts_model', 'qwen_tts.inference.qwen3_tts_tokenizer', 'qwen_tts.core', 'qwen_tts.cli', 'pkg_resources.extern']
|
||||||
|
datas += collect_data_files('qwen_tts')
|
||||||
|
datas += copy_metadata('qwen-tts')
|
||||||
|
hiddenimports += collect_submodules('qwen_tts')
|
||||||
|
hiddenimports += collect_submodules('jaraco')
|
||||||
|
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['server.py'],
|
||||||
|
pathex=[],
|
||||||
|
binaries=[],
|
||||||
|
datas=datas,
|
||||||
|
hiddenimports=hiddenimports,
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
noarchive=False,
|
||||||
|
optimize=0,
|
||||||
|
)
|
||||||
|
pyz = PYZ(a.pure)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
[],
|
||||||
|
name='voicebox-server-cuda',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
runtime_tmpdir=None,
|
||||||
|
console=True,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
)
|
||||||
@@ -4,15 +4,11 @@ from PyInstaller.utils.hooks import collect_submodules
|
|||||||
from PyInstaller.utils.hooks import copy_metadata
|
from PyInstaller.utils.hooks import copy_metadata
|
||||||
|
|
||||||
datas = []
|
datas = []
|
||||||
hiddenimports = ['backend', 'backend.main', 'backend.config', 'backend.database', 'backend.models', 'backend.profiles', 'backend.history', 'backend.tts', 'backend.transcribe', 'backend.platform_detect', 'backend.backends', 'backend.backends.pytorch_backend', 'backend.utils.audio', 'backend.utils.cache', 'backend.utils.progress', 'backend.utils.hf_progress', 'backend.utils.validation', 'torch', 'transformers', 'fastapi', 'uvicorn', 'sqlalchemy', 'librosa', 'soundfile', 'qwen_tts', 'qwen_tts.inference', 'qwen_tts.inference.qwen3_tts_model', 'qwen_tts.inference.qwen3_tts_tokenizer', 'qwen_tts.core', 'qwen_tts.cli', 'pkg_resources.extern', 'backend.backends.mlx_backend', 'mlx', 'mlx.core', 'mlx.nn', 'mlx_audio', 'mlx_audio.tts', 'mlx_audio.stt']
|
hiddenimports = ['backend', 'backend.main', 'backend.config', 'backend.database', 'backend.models', 'backend.profiles', 'backend.history', 'backend.tts', 'backend.transcribe', 'backend.platform_detect', 'backend.backends', 'backend.backends.pytorch_backend', 'backend.utils.audio', 'backend.utils.cache', 'backend.utils.progress', 'backend.utils.hf_progress', 'backend.utils.validation', 'torch', 'transformers', 'fastapi', 'uvicorn', 'sqlalchemy', 'librosa', 'soundfile', 'qwen_tts', 'qwen_tts.inference', 'qwen_tts.inference.qwen3_tts_model', 'qwen_tts.inference.qwen3_tts_tokenizer', 'qwen_tts.core', 'qwen_tts.cli', 'pkg_resources.extern']
|
||||||
datas += collect_data_files('qwen_tts')
|
datas += collect_data_files('qwen_tts')
|
||||||
datas += collect_data_files('mlx')
|
|
||||||
datas += collect_data_files('mlx_audio')
|
|
||||||
datas += copy_metadata('qwen-tts')
|
datas += copy_metadata('qwen-tts')
|
||||||
hiddenimports += collect_submodules('qwen_tts')
|
hiddenimports += collect_submodules('qwen_tts')
|
||||||
hiddenimports += collect_submodules('jaraco')
|
hiddenimports += collect_submodules('jaraco')
|
||||||
hiddenimports += collect_submodules('mlx')
|
|
||||||
hiddenimports += collect_submodules('mlx_audio')
|
|
||||||
|
|
||||||
|
|
||||||
a = Analysis(
|
a = Analysis(
|
||||||
|
|||||||
@@ -0,0 +1,620 @@
|
|||||||
|
# CUDA Distribution Problem - Complete Analysis
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
1. [Problem Overview](#problem-overview)
|
||||||
|
2. [Root Cause](#root-cause)
|
||||||
|
3. [Attempted Solutions](#attempted-solutions)
|
||||||
|
4. [Current Status](#current-status)
|
||||||
|
5. [Available Options](#available-options)
|
||||||
|
6. [Technical Details](#technical-details)
|
||||||
|
7. [Cost Analysis](#cost-analysis)
|
||||||
|
8. [Recommendations](#recommendations)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problem Overview
|
||||||
|
|
||||||
|
### Timeline of Issues
|
||||||
|
|
||||||
|
**Original Problem (v0.1.0 - v0.1.11)**
|
||||||
|
- Single server binary with CUDA support
|
||||||
|
- Size: ~2.9GB
|
||||||
|
- Issue: MSI installer build fails in GitHub Actions CI
|
||||||
|
- Error: WiX Toolset cannot handle 3GB files efficiently
|
||||||
|
|
||||||
|
**First Solution: Dual Binary System (v0.1.12)**
|
||||||
|
- Split into CPU (295MB) and CUDA (2.37GB) binaries
|
||||||
|
- CPU ships with installer
|
||||||
|
- CUDA as optional download
|
||||||
|
- Issue: GitHub Release assets have 2GB limit
|
||||||
|
|
||||||
|
**Current Problem (Discovered during implementation)**
|
||||||
|
- GitHub Release Asset Limit: **2GB hard maximum**
|
||||||
|
- CUDA binary: **2.37GB** (370MB over limit)
|
||||||
|
- Cannot upload to GitHub Releases
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Root Cause
|
||||||
|
|
||||||
|
### Why Is The CUDA Binary So Large?
|
||||||
|
|
||||||
|
The size difference between CPU and CUDA builds:
|
||||||
|
|
||||||
|
| Component | CPU Build | CUDA Build | Difference |
|
||||||
|
|-----------|-----------|------------|------------|
|
||||||
|
| PyTorch Core | ~150MB | ~150MB | - |
|
||||||
|
| CPU Libraries (MKL/OpenBLAS) | ~100MB | - | -100MB |
|
||||||
|
| CUDA Runtime | - | ~500MB | +500MB |
|
||||||
|
| cuBLAS | - | ~350MB | +350MB |
|
||||||
|
| cuDNN | - | ~1.2GB | +1.2GB |
|
||||||
|
| NVRTC (CUDA Compiler) | - | ~90MB | +90MB |
|
||||||
|
| Other CUDA libs | - | ~100MB | +100MB |
|
||||||
|
| **Total** | **~295MB** | **~2.37GB** | **+2.07GB** |
|
||||||
|
|
||||||
|
### CUDA Dependencies Breakdown
|
||||||
|
|
||||||
|
```
|
||||||
|
torch/lib/ (CUDA build):
|
||||||
|
├── cudart64_12.dll (~0.5 MB) - CUDA Runtime
|
||||||
|
├── cublas64_12.dll (~100 MB) - Basic Linear Algebra
|
||||||
|
├── cublasLt64_12.dll (~200 MB) - Linear Algebra (optimized)
|
||||||
|
├── cudnn64_9.dll (~800 MB) - Deep Neural Networks
|
||||||
|
├── cudnn_*_infer64_9.dll (~400 MB) - DNN Inference ops
|
||||||
|
├── nvrtc64_*.dll (~50 MB) - Runtime Compiler
|
||||||
|
├── nvrtc-builtins64_*.dll (~40 MB) - Compiler builtins
|
||||||
|
├── torch_cuda.dll (~200 MB) - PyTorch CUDA bridge
|
||||||
|
└── c10_cuda.dll (~20 MB) - Core CUDA utilities
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why These Are Required:**
|
||||||
|
- cuDNN is essential for neural network operations
|
||||||
|
- cuBLAS handles all matrix operations (core of ML)
|
||||||
|
- Cannot split or remove without breaking functionality
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Attempted Solutions
|
||||||
|
|
||||||
|
### Solution 1: Dual Binary System ✅ (Partially Successful)
|
||||||
|
|
||||||
|
**Goal**: Split CPU and CUDA into separate downloads
|
||||||
|
|
||||||
|
**Implementation**:
|
||||||
|
```bash
|
||||||
|
# Build CPU-only (295MB)
|
||||||
|
pip install torch --index-url https://download.pytorch.org/whl/cpu
|
||||||
|
python build_binary.py cpu
|
||||||
|
|
||||||
|
# Build CUDA (2.37GB)
|
||||||
|
pip install torch --index-url https://download.pytorch.org/whl/cu121
|
||||||
|
python build_binary.py cuda
|
||||||
|
```
|
||||||
|
|
||||||
|
**Results**:
|
||||||
|
- ✅ CPU binary: 295MB (fits in installer)
|
||||||
|
- ✅ CI builds successfully
|
||||||
|
- ✅ Installer size reduced from 3GB to ~500MB
|
||||||
|
- ❌ CUDA binary still too large for GitHub
|
||||||
|
|
||||||
|
**See**: `docs/dual-server-binaries.md`
|
||||||
|
|
||||||
|
### Solution 2: Compression Testing ❌ (Failed)
|
||||||
|
|
||||||
|
**Goal**: Compress CUDA binary to fit under 2GB
|
||||||
|
|
||||||
|
**Method**: 7z with maximum compression settings
|
||||||
|
```bash
|
||||||
|
7z a -t7z -m0=lzma2 -mx=9 -mfb=64 -md=32m -ms=on \
|
||||||
|
voicebox-server-cuda.7z voicebox-server-cuda.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
**Results**:
|
||||||
|
```
|
||||||
|
Original: 2.37 GB (2,545,086,396 bytes)
|
||||||
|
Compressed: 2.35 GB (2,519,381,264 bytes)
|
||||||
|
Compression: 1.0% (only 24.5MB saved)
|
||||||
|
GitHub Limit: 2.00 GB (2,147,483,648 bytes)
|
||||||
|
Over by: 354.67 MB
|
||||||
|
|
||||||
|
Status: FAILED - Still exceeds limit by 354MB
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why Compression Failed**:
|
||||||
|
- CUDA binaries are already optimized machine code
|
||||||
|
- No redundant data to compress
|
||||||
|
- Neural network kernels are highly compact
|
||||||
|
- Libraries are already stripped of debug symbols
|
||||||
|
|
||||||
|
**Conclusion**: Compression is not viable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current Status
|
||||||
|
|
||||||
|
### What Works
|
||||||
|
- ✅ CPU binary builds successfully (295MB)
|
||||||
|
- ✅ CUDA binary builds successfully (2.37GB)
|
||||||
|
- ✅ Build scripts for both variants
|
||||||
|
- ✅ CI workflow updated for dual binaries
|
||||||
|
- ✅ Installer can be created with CPU binary
|
||||||
|
|
||||||
|
### What Doesn't Work
|
||||||
|
- ❌ Cannot upload CUDA binary to GitHub Releases (exceeds 2GB limit)
|
||||||
|
- ❌ Compression doesn't reduce size enough
|
||||||
|
- ❌ No automated distribution path for CUDA binary
|
||||||
|
|
||||||
|
### Branch Status
|
||||||
|
- Branch: `feat/dual-server-binaries`
|
||||||
|
- Commits: Implementation complete
|
||||||
|
- Testing: Local builds successful
|
||||||
|
- Blocker: CUDA distribution path
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Available Options
|
||||||
|
|
||||||
|
### Option 1: AWS S3 Hosting (Recommended)
|
||||||
|
|
||||||
|
**Description**: Host CUDA binary in Amazon S3 bucket
|
||||||
|
|
||||||
|
**Pros**:
|
||||||
|
- ✅ No file size limits (can handle multi-GB files)
|
||||||
|
- ✅ Fast global CDN (CloudFront)
|
||||||
|
- ✅ Reliable (99.99% uptime)
|
||||||
|
- ✅ Pay only for usage
|
||||||
|
- ✅ Easy CI integration
|
||||||
|
- ✅ Version control (keep multiple releases)
|
||||||
|
|
||||||
|
**Cons**:
|
||||||
|
- ❌ Requires AWS account
|
||||||
|
- ❌ Monthly costs (~$1-5/month)
|
||||||
|
- ❌ Additional infrastructure to manage
|
||||||
|
|
||||||
|
**Cost Estimate**:
|
||||||
|
```
|
||||||
|
Storage: 2.37 GB × $0.023/GB = $0.05/month
|
||||||
|
Transfer: 100 downloads × 2.37GB × $0.09/GB = $21.33/month
|
||||||
|
Total: ~$21-25/month for 100 downloads
|
||||||
|
~$2-5/month for 10-20 downloads
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementation**:
|
||||||
|
```yaml
|
||||||
|
# .github/workflows/release.yml
|
||||||
|
- name: Upload CUDA to S3
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
|
run: |
|
||||||
|
aws s3 cp backend/cuda-release/voicebox-server-cuda-*.exe \
|
||||||
|
s3://voicebox-releases/cuda/${{ github.ref_name }}/ \
|
||||||
|
--acl public-read
|
||||||
|
|
||||||
|
# Generate download URL
|
||||||
|
echo "CUDA_URL=https://voicebox-releases.s3.amazonaws.com/cuda/${{ github.ref_name }}/voicebox-server-cuda-x86_64-pc-windows-msvc.exe" >> release_notes.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
**User Experience**:
|
||||||
|
1. Install app normally (500MB installer)
|
||||||
|
2. App detects NVIDIA GPU
|
||||||
|
3. Shows: "Download CUDA support? (2.4GB)"
|
||||||
|
4. Downloads from S3: `https://voicebox-releases.s3.amazonaws.com/cuda/v0.1.12/voicebox-server-cuda.exe`
|
||||||
|
5. Saves to `%APPDATA%/voicebox/binaries/`
|
||||||
|
6. App restarts with CUDA server
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Option 2: Azure Blob Storage
|
||||||
|
|
||||||
|
**Description**: Microsoft Azure alternative to S3
|
||||||
|
|
||||||
|
**Pros**:
|
||||||
|
- ✅ Similar to S3 (no size limits, CDN, reliable)
|
||||||
|
- ✅ Good if already using Azure
|
||||||
|
- ✅ Competitive pricing
|
||||||
|
- ✅ Global CDN with Azure CDN
|
||||||
|
|
||||||
|
**Cons**:
|
||||||
|
- ❌ Requires Azure account
|
||||||
|
- ❌ Similar monthly costs
|
||||||
|
- ❌ Less common in open source projects
|
||||||
|
|
||||||
|
**Cost Estimate**:
|
||||||
|
```
|
||||||
|
Storage: $0.018/GB = $0.04/month
|
||||||
|
Transfer: ~$20-25/month for 100 downloads
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementation**:
|
||||||
|
```yaml
|
||||||
|
- name: Upload to Azure Blob
|
||||||
|
env:
|
||||||
|
AZURE_STORAGE_CONNECTION_STRING: ${{ secrets.AZURE_STORAGE }}
|
||||||
|
run: |
|
||||||
|
az storage blob upload \
|
||||||
|
--account-name voiceboxreleases \
|
||||||
|
--container-name cuda-binaries \
|
||||||
|
--name v${{ github.ref_name }}/voicebox-server-cuda.exe \
|
||||||
|
--file backend/cuda-release/voicebox-server-cuda-*.exe \
|
||||||
|
--tier Hot
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Option 3: Cloudflare R2
|
||||||
|
|
||||||
|
**Description**: Cloudflare's S3-compatible object storage
|
||||||
|
|
||||||
|
**Pros**:
|
||||||
|
- ✅ S3-compatible API
|
||||||
|
- ✅ **FREE egress (no bandwidth charges!)**
|
||||||
|
- ✅ Cheaper than S3/Azure
|
||||||
|
- ✅ Cloudflare CDN included
|
||||||
|
- ✅ Good for open source projects
|
||||||
|
|
||||||
|
**Cons**:
|
||||||
|
- ❌ Requires Cloudflare account
|
||||||
|
- ❌ Newer service (less mature than S3)
|
||||||
|
|
||||||
|
**Cost Estimate**:
|
||||||
|
```
|
||||||
|
Storage: $0.015/GB = $0.04/month
|
||||||
|
Egress: $0.00 (FREE!)
|
||||||
|
Class A ops: Negligible
|
||||||
|
Total: ~$0.04/month (essentially free!)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why This Is Attractive**:
|
||||||
|
- Zero bandwidth costs (huge savings)
|
||||||
|
- Perfect for open source distribution
|
||||||
|
- S3-compatible (easy migration if needed)
|
||||||
|
|
||||||
|
**Implementation**:
|
||||||
|
Same as S3 (R2 is S3-compatible):
|
||||||
|
```yaml
|
||||||
|
- name: Upload to R2
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_ENDPOINT_URL: https://<account-id>.r2.cloudflarestorage.com
|
||||||
|
run: |
|
||||||
|
aws s3 cp backend/cuda-release/voicebox-server-cuda-*.exe \
|
||||||
|
s3://voicebox-releases/cuda/${{ github.ref_name }}/ \
|
||||||
|
--endpoint-url=$AWS_ENDPOINT_URL
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Option 4: GitHub Packages (Container Registry)
|
||||||
|
|
||||||
|
**Description**: Package CUDA binary as OCI/Docker artifact
|
||||||
|
|
||||||
|
**Pros**:
|
||||||
|
- ✅ Stays in GitHub ecosystem
|
||||||
|
- ✅ No additional accounts needed
|
||||||
|
- ✅ Free for public repos
|
||||||
|
|
||||||
|
**Cons**:
|
||||||
|
- ❌ Complex for desktop app distribution
|
||||||
|
- ❌ Users need to extract from container
|
||||||
|
- ❌ Awkward UX (not designed for binary distribution)
|
||||||
|
- ❌ Requires Docker understanding
|
||||||
|
|
||||||
|
**Not Recommended**: Containers aren't designed for desktop app binaries
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Option 5: Self-Hosted Server
|
||||||
|
|
||||||
|
**Description**: Host on your own VPS/server
|
||||||
|
|
||||||
|
**Pros**:
|
||||||
|
- ✅ Full control
|
||||||
|
- ✅ No cloud provider dependency
|
||||||
|
- ✅ Predictable costs
|
||||||
|
|
||||||
|
**Cons**:
|
||||||
|
- ❌ Requires server maintenance
|
||||||
|
- ❌ Bandwidth costs can be high
|
||||||
|
- ❌ Uptime responsibility
|
||||||
|
- ❌ Scaling challenges
|
||||||
|
|
||||||
|
**Cost Estimate**:
|
||||||
|
```
|
||||||
|
VPS: $5-20/month (DigitalOcean, Linode)
|
||||||
|
Bandwidth: $0.01-0.02/GB
|
||||||
|
Total: $10-50/month depending on traffic
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Option 6: Manual Distribution
|
||||||
|
|
||||||
|
**Description**: Don't automate - provide manual download instructions
|
||||||
|
|
||||||
|
**Pros**:
|
||||||
|
- ✅ Zero cost
|
||||||
|
- ✅ Zero infrastructure
|
||||||
|
- ✅ Simple
|
||||||
|
|
||||||
|
**Cons**:
|
||||||
|
- ❌ Poor user experience
|
||||||
|
- ❌ Manual upload to file host each release
|
||||||
|
- ❌ Users must manually download and install
|
||||||
|
- ❌ No automatic updates for CUDA binary
|
||||||
|
- ❌ Increases support burden
|
||||||
|
|
||||||
|
**Implementation**:
|
||||||
|
```
|
||||||
|
Release notes:
|
||||||
|
"Windows users with NVIDIA GPUs can download CUDA support:
|
||||||
|
1. Download voicebox-server-cuda.exe from [Google Drive/Mega/etc]
|
||||||
|
2. Place in C:\Users\<YourName>\AppData\Roaming\voicebox\binaries\
|
||||||
|
3. Restart the app"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Not Recommended**: Creates friction, support issues
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Option 7: Split CUDA Binary
|
||||||
|
|
||||||
|
**Description**: Break CUDA binary into multiple <2GB chunks
|
||||||
|
|
||||||
|
**Technical Approach**:
|
||||||
|
```python
|
||||||
|
# Split binary
|
||||||
|
split -b 2000M voicebox-server-cuda.exe cuda_part_
|
||||||
|
|
||||||
|
# Upload parts to GitHub (each <2GB)
|
||||||
|
cuda_part_aa (2.0 GB)
|
||||||
|
cuda_part_ab (0.37 GB)
|
||||||
|
|
||||||
|
# App downloads and reassembles
|
||||||
|
cat cuda_part_* > voicebox-server-cuda.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pros**:
|
||||||
|
- ✅ Stays on GitHub
|
||||||
|
- ✅ No external hosting
|
||||||
|
|
||||||
|
**Cons**:
|
||||||
|
- ❌ Complex download logic (multiple files)
|
||||||
|
- ❌ Integrity checking required
|
||||||
|
- ❌ More points of failure
|
||||||
|
- ❌ Users must wait for multiple downloads
|
||||||
|
- ❌ Still hacky solution
|
||||||
|
|
||||||
|
**Complexity**: Medium-High
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### Current Build Output
|
||||||
|
|
||||||
|
```
|
||||||
|
backend/dist/
|
||||||
|
├── voicebox-server.exe 295 MB (CPU-only)
|
||||||
|
└── voicebox-server-cuda.exe 2.37 GB (CUDA)
|
||||||
|
|
||||||
|
# After compression test:
|
||||||
|
backend/dist/
|
||||||
|
└── voicebox-server-cuda.7z 2.35 GB (not viable)
|
||||||
|
```
|
||||||
|
|
||||||
|
### CI Workflow Changes Required
|
||||||
|
|
||||||
|
For external hosting (S3/R2/Azure):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Current workflow (fails)
|
||||||
|
- name: Upload CUDA server binary (Windows only)
|
||||||
|
if: matrix.platform == 'windows-latest'
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
files: backend/cuda-release/voicebox-server-cuda-*.exe # ❌ Fails: >2GB
|
||||||
|
draft: true
|
||||||
|
|
||||||
|
# New workflow (S3 example)
|
||||||
|
- name: Upload CUDA to S3 (Windows only)
|
||||||
|
if: matrix.platform == 'windows-latest'
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
|
run: |
|
||||||
|
aws s3 cp backend/cuda-release/voicebox-server-cuda-*.exe \
|
||||||
|
s3://voicebox-releases/cuda/${{ github.ref_name }}/ \
|
||||||
|
--acl public-read
|
||||||
|
|
||||||
|
# Generate release notes with download URL
|
||||||
|
cat >> release_notes.md <<EOF
|
||||||
|
|
||||||
|
### GPU Acceleration (Windows)
|
||||||
|
Download CUDA support for NVIDIA GPUs:
|
||||||
|
[voicebox-server-cuda.exe](https://voicebox-releases.s3.amazonaws.com/cuda/${{ github.ref_name }}/voicebox-server-cuda-x86_64-pc-windows-msvc.exe)
|
||||||
|
Size: 2.37 GB
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
### App Changes Required
|
||||||
|
|
||||||
|
**Frontend (Tauri)**: Download manager
|
||||||
|
```typescript
|
||||||
|
// src/lib/cuda-downloader.ts
|
||||||
|
const CUDA_DOWNLOAD_URL =
|
||||||
|
"https://voicebox-releases.s3.amazonaws.com/cuda/v{VERSION}/voicebox-server-cuda.exe";
|
||||||
|
|
||||||
|
async function downloadCudaBinary(version: string) {
|
||||||
|
const url = CUDA_DOWNLOAD_URL.replace("{VERSION}", version);
|
||||||
|
const savePath = path.join(app.getPath("userData"), "binaries", "voicebox-server-cuda.exe");
|
||||||
|
|
||||||
|
// Download with progress
|
||||||
|
await downloadFile(url, savePath, (progress) => {
|
||||||
|
// Update UI: "Downloading CUDA support: 45% (1.2GB / 2.4GB)"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify checksum
|
||||||
|
const checksum = await calculateChecksum(savePath);
|
||||||
|
if (checksum !== EXPECTED_CHECKSUM) {
|
||||||
|
throw new Error("Download corrupted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Backend**: Already supports both binaries (no changes needed)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cost Analysis
|
||||||
|
|
||||||
|
### Monthly Cost Comparison (100 downloads/month)
|
||||||
|
|
||||||
|
| Option | Storage | Bandwidth | Total/Month | Notes |
|
||||||
|
|--------|---------|-----------|-------------|-------|
|
||||||
|
| **Cloudflare R2** | $0.04 | $0.00 | **$0.04** | Best for open source |
|
||||||
|
| AWS S3 | $0.05 | $21.33 | $21.38 | Good reliability |
|
||||||
|
| Azure Blob | $0.04 | $20.00 | $20.04 | Azure ecosystem |
|
||||||
|
| Self-hosted VPS | $10.00 | $2.37 | $12.37 | Maintenance overhead |
|
||||||
|
| Manual | $0.00 | $0.00 | $0.00 | Poor UX |
|
||||||
|
|
||||||
|
### Annual Cost Comparison
|
||||||
|
|
||||||
|
| Option | Year 1 | Year 2+ | Notes |
|
||||||
|
|--------|--------|---------|-------|
|
||||||
|
| **Cloudflare R2** | **$0.50** | **$0.50** | Essentially free |
|
||||||
|
| AWS S3 | $256 | $256 | Predictable |
|
||||||
|
| Self-hosted | $144 | $144 | Time cost |
|
||||||
|
|
||||||
|
**Recommendation**: Cloudflare R2 (free egress = huge savings)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
### Recommended Solution: Cloudflare R2
|
||||||
|
|
||||||
|
**Why**:
|
||||||
|
1. **Cost**: Essentially free (~$0.04/month)
|
||||||
|
2. **Bandwidth**: Zero egress charges (unlimited downloads)
|
||||||
|
3. **CDN**: Cloudflare's global network included
|
||||||
|
4. **Compatibility**: S3-compatible API (easy to use)
|
||||||
|
5. **Perfect for open source**: No surprise bandwidth bills
|
||||||
|
|
||||||
|
### Implementation Priority
|
||||||
|
|
||||||
|
**Phase 1: Setup (1-2 hours)**
|
||||||
|
1. Create Cloudflare R2 account
|
||||||
|
2. Create bucket: `voicebox-releases`
|
||||||
|
3. Generate API credentials
|
||||||
|
4. Add to GitHub Secrets
|
||||||
|
|
||||||
|
**Phase 2: CI Integration (1-2 hours)**
|
||||||
|
1. Update `.github/workflows/release.yml`
|
||||||
|
2. Add R2 upload step
|
||||||
|
3. Generate release notes with download URL
|
||||||
|
4. Test with draft release
|
||||||
|
|
||||||
|
**Phase 3: App Integration (4-6 hours)**
|
||||||
|
1. Add GPU detection on startup
|
||||||
|
2. Implement download manager UI
|
||||||
|
3. Add progress indicators
|
||||||
|
4. Implement checksum verification
|
||||||
|
5. Server restart logic
|
||||||
|
|
||||||
|
**Phase 4: Documentation (1 hour)**
|
||||||
|
1. Update README with GPU instructions
|
||||||
|
2. Add troubleshooting guide
|
||||||
|
3. Document manual download process
|
||||||
|
|
||||||
|
**Total Time**: ~8-12 hours of development
|
||||||
|
|
||||||
|
### Alternative: AWS S3 (If Already Using AWS)
|
||||||
|
|
||||||
|
If you're already using AWS for other infrastructure, S3 is also a solid choice:
|
||||||
|
- More mature than R2
|
||||||
|
- Extensive documentation
|
||||||
|
- Familiar tooling
|
||||||
|
- ~$20/month for moderate usage
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
1. **Expected Download Volume**: How many CUDA downloads per month?
|
||||||
|
- Affects cost calculations
|
||||||
|
- Determines if R2's free egress is significant
|
||||||
|
|
||||||
|
2. **Update Strategy**: How to handle CUDA updates?
|
||||||
|
- Option A: Version in URL path (keep all versions)
|
||||||
|
- Option B: Overwrite latest (save space)
|
||||||
|
|
||||||
|
3. **Fallback Strategy**: What if cloud provider is down?
|
||||||
|
- Mirror on multiple providers?
|
||||||
|
- Graceful degradation to CPU?
|
||||||
|
|
||||||
|
4. **Telemetry**: Track CUDA download stats?
|
||||||
|
- Helps with cost forecasting
|
||||||
|
- User behavior insights
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Research Phase** (You are here)
|
||||||
|
- Evaluate cloud providers
|
||||||
|
- Check terms of service
|
||||||
|
- Test account creation
|
||||||
|
|
||||||
|
2. **Decision Phase**
|
||||||
|
- Choose provider (Cloudflare R2 recommended)
|
||||||
|
- Set up account
|
||||||
|
- Configure billing alerts
|
||||||
|
|
||||||
|
3. **Implementation Phase**
|
||||||
|
- Update CI workflow
|
||||||
|
- Implement download manager
|
||||||
|
- Test end-to-end flow
|
||||||
|
|
||||||
|
4. **Launch Phase**
|
||||||
|
- Deploy to production
|
||||||
|
- Monitor downloads
|
||||||
|
- Gather user feedback
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- **GitHub Release Limits**: https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases
|
||||||
|
- **Cloudflare R2 Pricing**: https://developers.cloudflare.com/r2/pricing/
|
||||||
|
- **AWS S3 Pricing**: https://aws.amazon.com/s3/pricing/
|
||||||
|
- **Compression Test Results**: `backend/test_cuda_compression.py`
|
||||||
|
- **Dual Binary Implementation**: `docs/dual-server-binaries.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix: Alternative Approaches Considered
|
||||||
|
|
||||||
|
### A. Dynamic CUDA Loading
|
||||||
|
**Idea**: Load CUDA DLLs dynamically at runtime
|
||||||
|
**Why Not**: PyTorch requires CUDA DLLs at import time, can't lazy-load
|
||||||
|
|
||||||
|
### B. CUDA as Separate Package
|
||||||
|
**Idea**: Python package with just CUDA libs
|
||||||
|
**Why Not**: Still 2GB+, same problem
|
||||||
|
|
||||||
|
### C. Model Quantization
|
||||||
|
**Idea**: Use smaller quantized models
|
||||||
|
**Why Not**: Doesn't reduce CUDA runtime size
|
||||||
|
|
||||||
|
### D. Docker Distribution
|
||||||
|
**Idea**: Distribute as Docker container
|
||||||
|
**Why Not**: Poor fit for desktop app, requires Docker installed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Document Version**: 1.0
|
||||||
|
**Last Updated**: 2026-01-31
|
||||||
|
**Status**: Research Phase
|
||||||
|
**Next Review**: After cloud provider decision
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
# Dual Server Binary System
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Voicebox now uses a dual-binary approach to manage the size difference between CPU-only and CUDA-enabled builds:
|
||||||
|
|
||||||
|
- **CPU Binary** (~500MB): Ships with the installer by default
|
||||||
|
- **CUDA Binary** (~3GB): Downloaded on-demand for GPU users
|
||||||
|
|
||||||
|
## Problem Solved
|
||||||
|
|
||||||
|
Previously, bundling PyTorch with CUDA support created a 3GB server binary, which:
|
||||||
|
- Made the installer too large (failed CI builds with WiX)
|
||||||
|
- Forced all users to download CUDA libraries even without NVIDIA GPUs
|
||||||
|
- Created poor user experience
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
### Build Process
|
||||||
|
|
||||||
|
**Two separate binaries are built:**
|
||||||
|
|
||||||
|
1. **voicebox-server.exe** (CPU)
|
||||||
|
- Built with: `pip install torch --index-url https://download.pytorch.org/whl/cpu`
|
||||||
|
- Size: ~500MB
|
||||||
|
- Works on all Windows machines
|
||||||
|
- Included in the installer by default
|
||||||
|
|
||||||
|
2. **voicebox-server-cuda.exe** (CUDA)
|
||||||
|
- Built with: `pip install torch --index-url https://download.pytorch.org/whl/cu121`
|
||||||
|
- Size: ~3GB
|
||||||
|
- Requires NVIDIA GPU + drivers
|
||||||
|
- Uploaded as separate GitHub Release asset
|
||||||
|
|
||||||
|
### User Experience
|
||||||
|
|
||||||
|
**First Launch:**
|
||||||
|
1. User installs app (~500MB download)
|
||||||
|
2. App starts with CPU server
|
||||||
|
3. If NVIDIA GPU detected:
|
||||||
|
- Show notification: "Download CUDA support for 4-5x faster inference?"
|
||||||
|
- User clicks "Download"
|
||||||
|
- Download voicebox-server-cuda.exe from GitHub (~3GB)
|
||||||
|
- Save to `%APPDATA%/voicebox/binaries/`
|
||||||
|
- Restart server with CUDA version
|
||||||
|
|
||||||
|
**Settings Panel:**
|
||||||
|
- Toggle between CPU/CUDA modes
|
||||||
|
- Download CUDA if not already installed
|
||||||
|
- Show current inference backend
|
||||||
|
|
||||||
|
### Build Scripts
|
||||||
|
|
||||||
|
**Windows:**
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
|
||||||
|
# Build CPU only
|
||||||
|
build_cpu.bat
|
||||||
|
|
||||||
|
# Build CUDA only
|
||||||
|
build_cuda.bat
|
||||||
|
|
||||||
|
# Build both
|
||||||
|
build_both.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
**Unix (macOS/Linux):**
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
|
||||||
|
# Build CPU only
|
||||||
|
./build_cpu.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### CI/CD Workflow
|
||||||
|
|
||||||
|
**GitHub Actions (.github/workflows/release.yml):**
|
||||||
|
|
||||||
|
1. Install CPU PyTorch
|
||||||
|
2. Build CPU server → Copy to Tauri binaries
|
||||||
|
3. Install CUDA PyTorch
|
||||||
|
4. Build CUDA server → Save for upload
|
||||||
|
5. Build Tauri app (bundles CPU server)
|
||||||
|
6. Upload CUDA server as separate release asset
|
||||||
|
|
||||||
|
### File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
Release Assets:
|
||||||
|
├── Voicebox_0.1.12_x64_en-US.msi (~500MB - includes CPU server)
|
||||||
|
├── voicebox-server-cuda-x86_64-pc-windows-msvc.exe (~3GB - optional download)
|
||||||
|
└── latest.json (updater manifest)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### Modified Files
|
||||||
|
|
||||||
|
1. **backend/build_binary.py**
|
||||||
|
- Added `variant` parameter ('cpu' or 'cuda')
|
||||||
|
- Outputs different binary names based on variant
|
||||||
|
|
||||||
|
2. **backend/build_cpu.bat** (new)
|
||||||
|
- Installs CPU PyTorch
|
||||||
|
- Builds CPU binary
|
||||||
|
- Restores CUDA PyTorch for dev
|
||||||
|
|
||||||
|
3. **backend/build_cuda.bat** (new)
|
||||||
|
- Ensures CUDA PyTorch is installed
|
||||||
|
- Builds CUDA binary
|
||||||
|
|
||||||
|
4. **.github/workflows/release.yml**
|
||||||
|
- Build CPU binary first (for installer)
|
||||||
|
- Build CUDA binary second (for upload)
|
||||||
|
- Upload CUDA binary as additional release asset
|
||||||
|
- Updated release notes to explain GPU acceleration
|
||||||
|
|
||||||
|
### Future Frontend Work
|
||||||
|
|
||||||
|
**TODO: Implement CUDA download in the app**
|
||||||
|
|
||||||
|
Location: `tauri/src/`
|
||||||
|
|
||||||
|
Features needed:
|
||||||
|
1. GPU detection on startup
|
||||||
|
2. Download manager for CUDA binary
|
||||||
|
3. Server binary path switcher
|
||||||
|
4. Settings UI for CPU/CUDA toggle
|
||||||
|
5. Progress indicator for 3GB download
|
||||||
|
|
||||||
|
API endpoints needed (already exist):
|
||||||
|
- `/health` - Shows GPU availability
|
||||||
|
- Server restart mechanism
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
✓ **Smaller installer**: ~500MB instead of 3GB
|
||||||
|
✓ **Faster CI builds**: WiX can handle 500MB easily
|
||||||
|
✓ **User choice**: CPU users don't download unnecessary files
|
||||||
|
✓ **Better UX**: Optional performance upgrade for GPU users
|
||||||
|
✓ **Cost savings**: Reduced bandwidth for users without GPUs
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
**Test CPU build:**
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
python build_binary.py cpu
|
||||||
|
./dist/voicebox-server.exe --version
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test CUDA build:**
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
python build_binary.py cuda
|
||||||
|
./dist/voicebox-server-cuda.exe --version
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verify size:**
|
||||||
|
```bash
|
||||||
|
ls -lh backend/dist/
|
||||||
|
# Should see:
|
||||||
|
# voicebox-server.exe ~500MB
|
||||||
|
# voicebox-server-cuda.exe ~3GB
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test server startup:**
|
||||||
|
```bash
|
||||||
|
# CPU version
|
||||||
|
./backend/dist/voicebox-server.exe
|
||||||
|
# Check logs: Should show CPU inference
|
||||||
|
|
||||||
|
# CUDA version (requires NVIDIA GPU)
|
||||||
|
./backend/dist/voicebox-server-cuda.exe
|
||||||
|
# Check logs: Should show CUDA inference
|
||||||
|
```
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
# GitHub 2GB Release Asset Limit Issue
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
The CUDA server binary upload fails in CI with:
|
||||||
|
```
|
||||||
|
Error: File size (2543828017) is greater than 2 GiB
|
||||||
|
```
|
||||||
|
|
||||||
|
GitHub release assets have a hard limit of 2GB per file. Our CUDA binary is ~2.5GB, which exceeds this limit.
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
The dual-server binary system (see `dual-server-binaries.md`) creates two binaries:
|
||||||
|
- **CPU binary**: ~500MB ✅ Works fine
|
||||||
|
- **CUDA binary**: ~2.5GB ❌ Exceeds GitHub limit
|
||||||
|
|
||||||
|
## Attempted Solution: Compression
|
||||||
|
|
||||||
|
We're testing 7z compression with maximum settings to see if we can squeeze the CUDA binary under 2GB.
|
||||||
|
|
||||||
|
### Test Script
|
||||||
|
|
||||||
|
Run `backend/test_cuda_compression.py` to test compression locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
python test_cuda_compression.py
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
1. Find the CUDA binary in `dist/`
|
||||||
|
2. Compress it with 7z (maximum compression)
|
||||||
|
3. Report if the compressed size fits under 2GB
|
||||||
|
|
||||||
|
### Expected Compression
|
||||||
|
|
||||||
|
PyTorch CUDA binaries typically compress well since they contain:
|
||||||
|
- Repeated patterns in neural network weights
|
||||||
|
- Debug symbols and metadata
|
||||||
|
- Redundant CUDA libraries
|
||||||
|
|
||||||
|
Estimated compression: 30-40% reduction
|
||||||
|
- Original: ~2.5GB
|
||||||
|
- Target: <2GB
|
||||||
|
- Required compression: >20%
|
||||||
|
|
||||||
|
## Fallback: External Hosting
|
||||||
|
|
||||||
|
If compression doesn't work, we'll need to host the CUDA binary externally:
|
||||||
|
|
||||||
|
### Option 1: AWS S3
|
||||||
|
```yaml
|
||||||
|
- name: Upload CUDA binary to S3
|
||||||
|
run: |
|
||||||
|
aws s3 cp backend/cuda-release/voicebox-server-cuda-*.exe \
|
||||||
|
s3://voicebox-releases/cuda-binaries/${{ github.ref_name }}/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Azure Blob Storage
|
||||||
|
```yaml
|
||||||
|
- name: Upload to Azure Blob
|
||||||
|
run: |
|
||||||
|
az storage blob upload \
|
||||||
|
--account-name voiceboxreleases \
|
||||||
|
--container-name cuda-binaries \
|
||||||
|
--file backend/cuda-release/voicebox-server-cuda-*.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 3: GitHub Packages (Container Registry)
|
||||||
|
Package as a container image, though this adds complexity for desktop app distribution.
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
1. **Test compression locally** ← Current step
|
||||||
|
2. **If compression works (<2GB)**:
|
||||||
|
- Update CI to compress before upload
|
||||||
|
- Update app to handle .7z downloads
|
||||||
|
- Add extraction step in download manager
|
||||||
|
|
||||||
|
3. **If compression fails (≥2GB)**:
|
||||||
|
- Set up external storage (likely S3)
|
||||||
|
- Update CI to upload to S3
|
||||||
|
- Provide download URL in release notes
|
||||||
|
- Update app download manager to fetch from S3
|
||||||
|
|
||||||
|
## CI Workflow Changes (if compression works)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Compress CUDA binary (Windows only)
|
||||||
|
if: matrix.platform == 'windows-latest'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cd backend/cuda-release
|
||||||
|
7z a -t7z -m0=lzma2 -mx=9 -mfb=64 -md=32m -ms=on \
|
||||||
|
voicebox-server-cuda-x86_64-pc-windows-msvc.7z \
|
||||||
|
voicebox-server-cuda-*.exe
|
||||||
|
|
||||||
|
- name: Upload compressed CUDA server (Windows only)
|
||||||
|
if: matrix.platform == 'windows-latest'
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
files: backend/cuda-release/*.7z
|
||||||
|
```
|
||||||
|
|
||||||
|
## User Experience Impact
|
||||||
|
|
||||||
|
### With Compression
|
||||||
|
- Download: `voicebox-server-cuda-*.7z` (~1.5-1.8GB)
|
||||||
|
- App extracts automatically
|
||||||
|
- One extra step but manageable
|
||||||
|
|
||||||
|
### With External Hosting
|
||||||
|
- Download from S3/Azure URL
|
||||||
|
- No GitHub release asset dependency
|
||||||
|
- Potentially faster download speeds (CDN)
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
🔄 **Testing compression locally to determine viability**
|
||||||
|
|
||||||
|
Results pending from local test run.
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
# Cloudflare R2 Setup Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The CUDA binary (2.4GB) is hosted on Cloudflare R2 at `downloads.voicebox.sh` instead of GitHub Releases (which has a 2GB limit).
|
||||||
|
|
||||||
|
## R2 Bucket Configuration
|
||||||
|
|
||||||
|
✅ **Completed:**
|
||||||
|
- Bucket created: `voicebox`
|
||||||
|
- Custom domain configured: `downloads.voicebox.sh`
|
||||||
|
|
||||||
|
## GitHub Secrets Required
|
||||||
|
|
||||||
|
Add these secrets to your GitHub repository:
|
||||||
|
|
||||||
|
### 1. R2_ACCESS_KEY_ID
|
||||||
|
|
||||||
|
Your Cloudflare R2 API Access Key ID
|
||||||
|
|
||||||
|
**How to get it:**
|
||||||
|
1. Go to Cloudflare Dashboard → R2
|
||||||
|
2. Click "Manage R2 API Tokens"
|
||||||
|
3. Create API Token with "Object Read & Write" permissions
|
||||||
|
4. Copy the "Access Key ID"
|
||||||
|
|
||||||
|
**Add to GitHub:**
|
||||||
|
```
|
||||||
|
Repository Settings → Secrets and variables → Actions → New repository secret
|
||||||
|
Name: R2_ACCESS_KEY_ID
|
||||||
|
Value: <your-access-key-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. R2_SECRET_ACCESS_KEY
|
||||||
|
|
||||||
|
Your Cloudflare R2 Secret Access Key
|
||||||
|
|
||||||
|
**How to get it:**
|
||||||
|
- Same process as above
|
||||||
|
- Copy the "Secret Access Key" (shown only once!)
|
||||||
|
- Store it securely
|
||||||
|
|
||||||
|
**Add to GitHub:**
|
||||||
|
```
|
||||||
|
Name: R2_SECRET_ACCESS_KEY
|
||||||
|
Value: <your-secret-access-key>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. R2_ENDPOINT
|
||||||
|
|
||||||
|
Your Cloudflare R2 endpoint URL
|
||||||
|
|
||||||
|
**Format:**
|
||||||
|
```
|
||||||
|
https://<account-id>.r2.cloudflarestorage.com
|
||||||
|
```
|
||||||
|
|
||||||
|
**How to find your account ID:**
|
||||||
|
- Cloudflare Dashboard → R2
|
||||||
|
- Look at the URL or bucket settings
|
||||||
|
- Should be a string of letters/numbers
|
||||||
|
|
||||||
|
**Add to GitHub:**
|
||||||
|
```
|
||||||
|
Name: R2_ENDPOINT
|
||||||
|
Value: https://<your-account-id>.r2.cloudflarestorage.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bucket Structure
|
||||||
|
|
||||||
|
After CI uploads, the bucket will have this structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
voicebox/
|
||||||
|
└── cuda/
|
||||||
|
├── v0.1.12/
|
||||||
|
│ └── voicebox-server-cuda-x86_64-pc-windows-msvc.exe
|
||||||
|
├── v0.1.13/
|
||||||
|
│ └── voicebox-server-cuda-x86_64-pc-windows-msvc.exe
|
||||||
|
└── v0.2.0/
|
||||||
|
└── voicebox-server-cuda-x86_64-pc-windows-msvc.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
## Public Access
|
||||||
|
|
||||||
|
Files are uploaded with `--acl public-read`, making them accessible at:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://downloads.voicebox.sh/cuda/v{VERSION}/voicebox-server-cuda-x86_64-pc-windows-msvc.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```
|
||||||
|
https://downloads.voicebox.sh/cuda/v0.1.12/voicebox-server-cuda-x86_64-pc-windows-msvc.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing the Setup
|
||||||
|
|
||||||
|
### Local Test Upload
|
||||||
|
|
||||||
|
Before running the CI, test uploading locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set environment variables
|
||||||
|
export AWS_ACCESS_KEY_ID="your-r2-access-key-id"
|
||||||
|
export AWS_SECRET_ACCESS_KEY="your-r2-secret-access-key"
|
||||||
|
export R2_ENDPOINT="https://your-account-id.r2.cloudflarestorage.com"
|
||||||
|
|
||||||
|
# Install AWS CLI
|
||||||
|
pip install awscli
|
||||||
|
|
||||||
|
# Test upload (use a small test file first)
|
||||||
|
echo "test" > test.txt
|
||||||
|
aws s3 cp test.txt \
|
||||||
|
s3://voicebox/test/test.txt \
|
||||||
|
--endpoint-url $R2_ENDPOINT \
|
||||||
|
--acl public-read
|
||||||
|
|
||||||
|
# Verify it's accessible
|
||||||
|
curl https://downloads.voicebox.sh/test/test.txt
|
||||||
|
|
||||||
|
# If successful, try the actual CUDA binary
|
||||||
|
aws s3 cp backend/dist/voicebox-server-cuda.exe \
|
||||||
|
s3://voicebox/cuda/v0.1.12-test/voicebox-server-cuda-x86_64-pc-windows-msvc.exe \
|
||||||
|
--endpoint-url $R2_ENDPOINT \
|
||||||
|
--acl public-read
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify Upload
|
||||||
|
|
||||||
|
Check if the file is accessible:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -I https://downloads.voicebox.sh/cuda/v0.1.12-test/voicebox-server-cuda-x86_64-pc-windows-msvc.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
Should return:
|
||||||
|
```
|
||||||
|
HTTP/2 200
|
||||||
|
content-length: 2545086396
|
||||||
|
content-type: application/x-msdownload
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI Workflow
|
||||||
|
|
||||||
|
The workflow now:
|
||||||
|
|
||||||
|
1. **Builds CPU binary** → Includes in installer
|
||||||
|
2. **Builds CUDA binary** → Uploads to R2
|
||||||
|
3. **Release notes** → Include R2 download link
|
||||||
|
|
||||||
|
### CI Steps (Windows)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Build CUDA Python server (Windows only)
|
||||||
|
# Builds the CUDA binary
|
||||||
|
|
||||||
|
- name: Upload CUDA server to Cloudflare R2 (Windows only)
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||||
|
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||||
|
run: |
|
||||||
|
aws s3 cp backend/cuda-release/voicebox-server-cuda-*.exe \
|
||||||
|
s3://voicebox/cuda/${VERSION}/... \
|
||||||
|
--endpoint-url $R2_ENDPOINT \
|
||||||
|
--acl public-read
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cost Tracking
|
||||||
|
|
||||||
|
Monitor your R2 usage:
|
||||||
|
|
||||||
|
**Cloudflare Dashboard → R2 → voicebox → Metrics**
|
||||||
|
|
||||||
|
Expected costs (per month):
|
||||||
|
- Storage: 2.4GB × $0.015/GB = **$0.036**
|
||||||
|
- Egress: **$0.00** (free!)
|
||||||
|
- Class A ops: ~100 × $4.50/million = **$0.00**
|
||||||
|
|
||||||
|
**Total: ~$0.04/month** (essentially free!)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Upload fails: "Access Denied"
|
||||||
|
|
||||||
|
**Solution:** Check API token permissions
|
||||||
|
- Must have "Object Read & Write" on the bucket
|
||||||
|
- Regenerate token if needed
|
||||||
|
|
||||||
|
### File not accessible at downloads.voicebox.sh
|
||||||
|
|
||||||
|
**Solution:** Check custom domain configuration
|
||||||
|
- R2 Dashboard → Bucket → Settings → Custom Domains
|
||||||
|
- Ensure `downloads.voicebox.sh` is properly configured
|
||||||
|
- DNS may take time to propagate
|
||||||
|
|
||||||
|
### "endpoint-url" not recognized
|
||||||
|
|
||||||
|
**Solution:** Make sure AWS CLI is updated
|
||||||
|
```bash
|
||||||
|
pip install --upgrade awscli
|
||||||
|
```
|
||||||
|
|
||||||
|
### File uploaded but wrong permissions
|
||||||
|
|
||||||
|
**Solution:** Re-upload with `--acl public-read`
|
||||||
|
```bash
|
||||||
|
aws s3 cp ... --acl public-read
|
||||||
|
```
|
||||||
|
|
||||||
|
Or set bucket default permissions in R2 Dashboard.
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
### API Token Permissions
|
||||||
|
|
||||||
|
✅ **Recommended:**
|
||||||
|
- Object Read & Write only
|
||||||
|
- No admin permissions needed
|
||||||
|
- Scoped to `voicebox` bucket only
|
||||||
|
|
||||||
|
❌ **Avoid:**
|
||||||
|
- Account-wide permissions
|
||||||
|
- Account admin access
|
||||||
|
- Worker edit permissions
|
||||||
|
|
||||||
|
### Secret Rotation
|
||||||
|
|
||||||
|
Rotate API tokens every 6-12 months:
|
||||||
|
1. Create new API token
|
||||||
|
2. Update GitHub secrets
|
||||||
|
3. Verify CI still works
|
||||||
|
4. Delete old token
|
||||||
|
|
||||||
|
## Maintenance
|
||||||
|
|
||||||
|
### Cleaning Old Versions
|
||||||
|
|
||||||
|
Optional: Delete old CUDA binaries to save storage costs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all versions
|
||||||
|
aws s3 ls s3://voicebox/cuda/ \
|
||||||
|
--endpoint-url $R2_ENDPOINT
|
||||||
|
|
||||||
|
# Delete old version
|
||||||
|
aws s3 rm s3://voicebox/cuda/v0.1.0/ \
|
||||||
|
--recursive \
|
||||||
|
--endpoint-url $R2_ENDPOINT
|
||||||
|
```
|
||||||
|
|
||||||
|
### Monitoring
|
||||||
|
|
||||||
|
Set up Cloudflare notifications:
|
||||||
|
- Storage approaching limits
|
||||||
|
- Unusual traffic patterns
|
||||||
|
- High operation counts
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. ✅ Bucket configured
|
||||||
|
2. ⏳ Add GitHub secrets (R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_ENDPOINT)
|
||||||
|
3. ⏳ Test local upload
|
||||||
|
4. ⏳ Push branch and create test release
|
||||||
|
5. ⏳ Verify CUDA binary accessible from downloads.voicebox.sh
|
||||||
|
6. ⏳ Implement frontend download manager
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Status**: Ready for testing
|
||||||
|
**Cost**: ~$0.04/month
|
||||||
|
**Bandwidth**: Free (unlimited)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@voicebox/landing",
|
"name": "@voicebox/landing",
|
||||||
"version": "0.1.11",
|
"version": "0.1.12",
|
||||||
"description": "Landing page for voicebox.sh",
|
"description": "Landing page for voicebox.sh",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun --bun next dev --turbo",
|
"dev": "bun --bun next dev --turbo",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "voicebox",
|
"name": "voicebox",
|
||||||
"version": "0.1.11",
|
"version": "0.1.12",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"app",
|
"app",
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@voicebox/tauri",
|
"name": "@voicebox/tauri",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.11",
|
"version": "0.1.12",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
Generated
+1
-1
@@ -5041,7 +5041,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "voicebox"
|
name = "voicebox"
|
||||||
version = "0.1.11"
|
version = "0.1.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "voicebox"
|
name = "voicebox"
|
||||||
version = "0.1.11"
|
version = "0.1.12"
|
||||||
description = "A production-quality desktop app for Qwen3-TTS voice cloning and generation"
|
description = "A production-quality desktop app for Qwen3-TTS voice cloning and generation"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
license = ""
|
license = ""
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Voicebox",
|
"productName": "Voicebox",
|
||||||
"version": "0.1.11",
|
"version": "0.1.12",
|
||||||
"identifier": "sh.voicebox.app",
|
"identifier": "sh.voicebox.app",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "bun run dev",
|
"beforeDevCommand": "bun run dev",
|
||||||
|
|||||||
@@ -35,5 +35,15 @@ export default defineConfig({
|
|||||||
minify: !process.env.TAURI_DEBUG,
|
minify: !process.env.TAURI_DEBUG,
|
||||||
sourcemap: !!process.env.TAURI_DEBUG,
|
sourcemap: !!process.env.TAURI_DEBUG,
|
||||||
outDir: 'dist',
|
outDir: 'dist',
|
||||||
|
rollupOptions: {
|
||||||
|
external: [
|
||||||
|
'@tauri-apps/api',
|
||||||
|
'@tauri-apps/plugin-dialog',
|
||||||
|
'@tauri-apps/plugin-fs',
|
||||||
|
'@tauri-apps/plugin-process',
|
||||||
|
'@tauri-apps/plugin-shell',
|
||||||
|
'@tauri-apps/plugin-updater',
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
"""Test CUDA detection in voicebox backend"""
|
||||||
|
import sys
|
||||||
|
import torch
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("PyTorch CUDA Detection Test")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Basic torch info
|
||||||
|
print(f"\nPyTorch version: {torch.__version__}")
|
||||||
|
print(f"CUDA available: {torch.cuda.is_available()}")
|
||||||
|
|
||||||
|
if torch.cuda.is_available():
|
||||||
|
print(f"CUDA version: {torch.version.cuda}")
|
||||||
|
print(f"GPU count: {torch.cuda.device_count()}")
|
||||||
|
print(f"Current GPU: {torch.cuda.current_device()}")
|
||||||
|
print(f"GPU name: {torch.cuda.get_device_name(0)}")
|
||||||
|
print(f"GPU memory: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.2f} GB")
|
||||||
|
else:
|
||||||
|
print("\nNo CUDA available - would run on CPU")
|
||||||
|
|
||||||
|
# Test backend device selection
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("Backend Device Selection")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Simulate the _get_device method from pytorch_backend.py
|
||||||
|
def _get_device() -> str:
|
||||||
|
"""Get the best available device."""
|
||||||
|
if torch.cuda.is_available():
|
||||||
|
return "cuda"
|
||||||
|
elif hasattr(torch.backends, 'mps') and torch.backends.mps.is_available():
|
||||||
|
# MPS can have issues, use CPU for stability
|
||||||
|
return "cpu"
|
||||||
|
return "cpu"
|
||||||
|
|
||||||
|
selected_device = _get_device()
|
||||||
|
print(f"\nSelected device: {selected_device}")
|
||||||
|
print(f"Would use dtype: {'torch.bfloat16' if selected_device != 'cpu' else 'torch.float32'}")
|
||||||
|
|
||||||
|
# Test actual tensor creation on device
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("Testing Tensor Creation on Device")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
try:
|
||||||
|
test_tensor = torch.randn(1000, 1000).to(selected_device)
|
||||||
|
print(f"\n[OK] Successfully created tensor on {selected_device}")
|
||||||
|
print(f" Tensor device: {test_tensor.device}")
|
||||||
|
print(f" Tensor dtype: {test_tensor.dtype}")
|
||||||
|
|
||||||
|
# Test computation
|
||||||
|
result = test_tensor @ test_tensor.T
|
||||||
|
print(f"[OK] Successfully performed computation on {selected_device}")
|
||||||
|
|
||||||
|
if selected_device == "cuda":
|
||||||
|
print(f"\nCUDA memory allocated: {torch.cuda.memory_allocated() / 1024**2:.2f} MB")
|
||||||
|
print(f"CUDA memory reserved: {torch.cuda.memory_reserved() / 1024**2:.2f} MB")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n[ERROR] {e}")
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("Summary")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
if selected_device == "cuda":
|
||||||
|
print("\n[SUCCESS] CUDA IS WORKING!")
|
||||||
|
print(" The backend will use your NVIDIA GPU for inference")
|
||||||
|
print(f" GPU: {torch.cuda.get_device_name(0)}")
|
||||||
|
print(f" This will be significantly faster than CPU")
|
||||||
|
else:
|
||||||
|
print("\n[FAIL] CUDA is not available")
|
||||||
|
print(" The backend will use CPU for inference")
|
||||||
|
print(" This will be slower than GPU")
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@voicebox/web",
|
"name": "@voicebox/web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.11",
|
"version": "0.1.12",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
Reference in New Issue
Block a user