mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-29 15:15:27 -07:00
Enhance provider packaging and CUDA download functionality
- Added support for packaging provider archives in the release workflow, creating platform-specific zip and tar.gz files for distribution. - Updated the `.gitignore` to exclude `.spec` files. - Introduced a new `CudaDownloadSection` component to manage CUDA downloads, including progress tracking and error handling. - Refactored provider download logic to handle archive extraction and cleanup after download. - Improved subprocess output handling in the provider manager for better logging and error reporting.
This commit is contained in:
@@ -91,6 +91,30 @@ jobs:
|
|||||||
cd providers/${{ matrix.provider }}
|
cd providers/${{ matrix.provider }}
|
||||||
python build.py
|
python build.py
|
||||||
|
|
||||||
|
- name: Package provider for distribution
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cd providers/${{ matrix.provider }}/dist
|
||||||
|
|
||||||
|
# Add platform suffix for archive name
|
||||||
|
if [ "${{ matrix.platform }}" == "windows-latest" ]; then
|
||||||
|
ARCHIVE_NAME="tts-provider-${{ matrix.provider }}-windows.zip"
|
||||||
|
# On Windows, zip the directory
|
||||||
|
powershell Compress-Archive -Path "tts-provider-${{ matrix.provider }}/*" -DestinationPath "$ARCHIVE_NAME"
|
||||||
|
elif [ "${{ matrix.platform }}" == "macos-latest" ]; then
|
||||||
|
ARCHIVE_NAME="tts-provider-${{ matrix.provider }}-macos-arm64.tar.gz"
|
||||||
|
tar -czf "$ARCHIVE_NAME" tts-provider-${{ matrix.provider }}/
|
||||||
|
elif [ "${{ matrix.platform }}" == "macos-15-intel" ]; then
|
||||||
|
ARCHIVE_NAME="tts-provider-${{ matrix.provider }}-macos-x64.tar.gz"
|
||||||
|
tar -czf "$ARCHIVE_NAME" tts-provider-${{ matrix.provider }}/
|
||||||
|
else
|
||||||
|
ARCHIVE_NAME="tts-provider-${{ matrix.provider }}-linux.tar.gz"
|
||||||
|
tar -czf "$ARCHIVE_NAME" tts-provider-${{ matrix.provider }}/
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Created archive: $ARCHIVE_NAME"
|
||||||
|
ls -lh "$ARCHIVE_NAME"
|
||||||
|
|
||||||
- name: Upload provider to R2
|
- name: Upload provider to R2
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
@@ -106,31 +130,23 @@ jobs:
|
|||||||
aws configure set aws_secret_access_key $R2_SECRET_ACCESS_KEY
|
aws configure set aws_secret_access_key $R2_SECRET_ACCESS_KEY
|
||||||
aws configure set region auto
|
aws configure set region auto
|
||||||
|
|
||||||
# Determine binary name based on platform
|
# Determine archive name based on platform
|
||||||
if [ "${{ matrix.platform }}" == "windows-latest" ]; then
|
if [ "${{ matrix.platform }}" == "windows-latest" ]; then
|
||||||
BINARY_NAME="tts-provider-${{ matrix.provider }}.exe"
|
ARCHIVE_NAME="tts-provider-${{ matrix.provider }}-windows.zip"
|
||||||
BINARY_PATH="providers/${{ matrix.provider }}/dist/tts-provider-${{ matrix.provider }}.exe"
|
|
||||||
else
|
|
||||||
BINARY_NAME="tts-provider-${{ matrix.provider }}"
|
|
||||||
BINARY_PATH="providers/${{ matrix.provider }}/dist/tts-provider-${{ matrix.provider }}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Add platform suffix for clarity
|
|
||||||
if [ "${{ matrix.platform }}" == "windows-latest" ]; then
|
|
||||||
UPLOAD_NAME="tts-provider-${{ matrix.provider }}-windows.exe"
|
|
||||||
elif [ "${{ matrix.platform }}" == "macos-latest" ]; then
|
elif [ "${{ matrix.platform }}" == "macos-latest" ]; then
|
||||||
UPLOAD_NAME="tts-provider-${{ matrix.provider }}-macos-arm64"
|
ARCHIVE_NAME="tts-provider-${{ matrix.provider }}-macos-arm64.tar.gz"
|
||||||
elif [ "${{ matrix.platform }}" == "macos-15-intel" ]; then
|
elif [ "${{ matrix.platform }}" == "macos-15-intel" ]; then
|
||||||
UPLOAD_NAME="tts-provider-${{ matrix.provider }}-macos-x64"
|
ARCHIVE_NAME="tts-provider-${{ matrix.provider }}-macos-x64.tar.gz"
|
||||||
else
|
else
|
||||||
UPLOAD_NAME="tts-provider-${{ matrix.provider }}-linux"
|
ARCHIVE_NAME="tts-provider-${{ matrix.provider }}-linux.tar.gz"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Upload to R2 (bucket: voicebox)
|
# Upload to R2 (bucket: voicebox)
|
||||||
aws s3 cp "$BINARY_PATH" "s3://voicebox/providers/v${{ env.PROVIDER_VERSION }}/$UPLOAD_NAME" \
|
aws s3 cp "providers/${{ matrix.provider }}/dist/$ARCHIVE_NAME" \
|
||||||
|
"s3://voicebox/providers/v${{ env.PROVIDER_VERSION }}/$ARCHIVE_NAME" \
|
||||||
--endpoint-url "$R2_ENDPOINT"
|
--endpoint-url "$R2_ENDPOINT"
|
||||||
|
|
||||||
echo "Uploaded $UPLOAD_NAME to R2"
|
echo "Uploaded $ARCHIVE_NAME to R2"
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Build Main App (without bundled TTS on Win/Linux)
|
# Build Main App (without bundled TTS on Win/Linux)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ dist/
|
|||||||
build/
|
build/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
*.egg
|
*.egg
|
||||||
|
*.spec
|
||||||
target/
|
target/
|
||||||
*.app
|
*.app
|
||||||
*.dmg
|
*.dmg
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { Loader2, RefreshCw, CheckCircle, AlertCircle } from 'lucide-react';
|
||||||
|
import { relaunch } from '@tauri-apps/plugin-process';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
import { ModelProgress } from './ModelProgress';
|
||||||
|
import { apiClient } from '@/lib/api/client';
|
||||||
|
import { useServerStore } from '@/stores/serverStore';
|
||||||
|
import { toast } from '@/components/ui/use-toast';
|
||||||
|
import { usePlatform } from '@/platform/PlatformContext';
|
||||||
|
import { useServerHealth } from '@/lib/hooks/useServer';
|
||||||
|
|
||||||
|
export function CudaDownloadSection() {
|
||||||
|
const platform = usePlatform();
|
||||||
|
const serverUrl = useServerStore((state) => state.serverUrl);
|
||||||
|
const { data: health } = useServerHealth();
|
||||||
|
const [downloadingCuda, setDownloadingCuda] = useState(false);
|
||||||
|
const [downloadError, setDownloadError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Platform checks
|
||||||
|
const isWindows = navigator.userAgent.includes('Windows');
|
||||||
|
const gpuAvailable = health?.gpu_available ?? false;
|
||||||
|
|
||||||
|
// Only show this section on Windows + Tauri + GPU available
|
||||||
|
if (!platform.metadata.isTauri || !isWindows || !gpuAvailable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query CUDA status
|
||||||
|
const { data: cudaStatus, refetch } = useQuery({
|
||||||
|
queryKey: ['cudaStatus'],
|
||||||
|
queryFn: () => apiClient.getCudaStatus(),
|
||||||
|
enabled: isWindows && platform.metadata.isTauri && gpuAvailable,
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle download trigger
|
||||||
|
const handleDownload = async () => {
|
||||||
|
try {
|
||||||
|
setDownloadError(null);
|
||||||
|
await apiClient.triggerCudaDownload();
|
||||||
|
setDownloadingCuda(true);
|
||||||
|
} catch (error) {
|
||||||
|
setDownloadError(error instanceof Error ? error.message : 'Failed to start download');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle retry
|
||||||
|
const handleRetry = () => {
|
||||||
|
setDownloadError(null);
|
||||||
|
handleDownload();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Monitor download progress and auto-restart on completion
|
||||||
|
useEffect(() => {
|
||||||
|
if (!downloadingCuda || !serverUrl) return;
|
||||||
|
|
||||||
|
const eventSource = new EventSource(`${serverUrl}/models/progress/cuda-binary`);
|
||||||
|
|
||||||
|
eventSource.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
|
if (data.status === 'complete' && data.progress >= 100) {
|
||||||
|
eventSource.close();
|
||||||
|
setDownloadingCuda(false);
|
||||||
|
|
||||||
|
// Show restart toast
|
||||||
|
toast({
|
||||||
|
title: 'CUDA Downloaded',
|
||||||
|
description: 'Restarting app to enable GPU acceleration...',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restart after 2 seconds
|
||||||
|
setTimeout(async () => {
|
||||||
|
await relaunch();
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.status === 'error') {
|
||||||
|
eventSource.close();
|
||||||
|
setDownloadingCuda(false);
|
||||||
|
setDownloadError(data.error || 'Download failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing CUDA download progress:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.onerror = () => {
|
||||||
|
eventSource.close();
|
||||||
|
setDownloadingCuda(false);
|
||||||
|
setDownloadError('Connection to server lost');
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => eventSource.close();
|
||||||
|
}, [downloadingCuda, serverUrl]);
|
||||||
|
|
||||||
|
if (!cudaStatus) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { cuda_available, cuda_active, cuda_binary_size_mb } = cudaStatus;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>GPU Acceleration</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{/* Status badges */}
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Badge variant={cuda_active ? 'default' : 'secondary'}>
|
||||||
|
Mode: {cuda_active ? 'CUDA' : 'CPU'}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant={gpuAvailable ? 'default' : 'secondary'}>
|
||||||
|
GPU: {gpuAvailable ? 'Available' : 'Not Available'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Already downloaded status */}
|
||||||
|
{cuda_available && !downloadingCuda && (
|
||||||
|
<Alert>
|
||||||
|
<CheckCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
CUDA support is active. GPU acceleration enabled.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Download section - only show if not already downloaded */}
|
||||||
|
{!cuda_available && !downloadError && !downloadingCuda && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Download CUDA support for 4-5x faster inference with your NVIDIA GPU
|
||||||
|
</p>
|
||||||
|
<Button onClick={handleDownload} className="w-full">
|
||||||
|
Download CUDA Support ({cuda_binary_size_mb.toFixed(1)}GB)
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Progress display */}
|
||||||
|
{downloadingCuda && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<ModelProgress
|
||||||
|
modelName="cuda-binary"
|
||||||
|
displayName="CUDA Server Binary"
|
||||||
|
isDownloading={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error with retry button */}
|
||||||
|
{downloadError && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription>{downloadError}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
<Button onClick={handleRetry} variant="outline" className="w-full">
|
||||||
|
<RefreshCw className="mr-2 h-4 w-4" />
|
||||||
|
Retry Download
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -100,13 +100,43 @@ class ProviderManager:
|
|||||||
stdout_content = ""
|
stdout_content = ""
|
||||||
stderr_content = ""
|
stderr_content = ""
|
||||||
|
|
||||||
|
# Try to read available output (works on Windows and Unix)
|
||||||
try:
|
try:
|
||||||
if stdout_log.exists():
|
# Use non-blocking read with timeout
|
||||||
stdout_content = stdout_log.read_text()
|
import threading
|
||||||
if stderr_log.exists():
|
import queue
|
||||||
stderr_content = stderr_log.read_text()
|
|
||||||
except Exception as read_err:
|
def enqueue_output(stream, queue):
|
||||||
logger.error(f"Failed to read provider logs: {read_err}")
|
try:
|
||||||
|
for line in iter(stream.readline, ''):
|
||||||
|
queue.put(line)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
stdout_queue = queue.Queue()
|
||||||
|
stderr_queue = queue.Queue()
|
||||||
|
|
||||||
|
if process.stdout:
|
||||||
|
t = threading.Thread(target=enqueue_output, args=(process.stdout, stdout_queue))
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
if process.stderr:
|
||||||
|
t2 = threading.Thread(target=enqueue_output, args=(process.stderr, stderr_queue))
|
||||||
|
t2.daemon = True
|
||||||
|
t2.start()
|
||||||
|
|
||||||
|
# Give threads a moment to read
|
||||||
|
import time
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# Collect output
|
||||||
|
while not stdout_queue.empty():
|
||||||
|
stdout_lines.append(stdout_queue.get_nowait())
|
||||||
|
while not stderr_queue.empty():
|
||||||
|
stderr_lines.append(stderr_queue.get_nowait())
|
||||||
|
except Exception as ex:
|
||||||
|
logger.warning(f"Could not capture subprocess output: {ex}")
|
||||||
|
|
||||||
logger.error(f"Provider failed to start within 30 seconds")
|
logger.error(f"Provider failed to start within 30 seconds")
|
||||||
logger.error(f"Check logs at: {logs_dir}")
|
logger.error(f"Check logs at: {logs_dir}")
|
||||||
|
|||||||
@@ -55,13 +55,13 @@ def _get_provider_binary_name(provider_type: str) -> str:
|
|||||||
def _get_provider_download_name(provider_type: str) -> str:
|
def _get_provider_download_name(provider_type: str) -> str:
|
||||||
"""Get the remote download filename for a provider type (includes platform suffix)."""
|
"""Get the remote download filename for a provider type (includes platform suffix)."""
|
||||||
system = platform.system()
|
system = platform.system()
|
||||||
|
|
||||||
if system == "Windows":
|
if system == "Windows":
|
||||||
platform_suffix = "windows"
|
platform_suffix = "windows"
|
||||||
ext = ".exe"
|
ext = ".zip"
|
||||||
elif system == "Linux":
|
elif system == "Linux":
|
||||||
platform_suffix = "linux"
|
platform_suffix = "linux"
|
||||||
ext = ""
|
ext = ".tar.gz"
|
||||||
elif system == "Darwin":
|
elif system == "Darwin":
|
||||||
# Detect macOS architecture
|
# Detect macOS architecture
|
||||||
machine = platform.machine()
|
machine = platform.machine()
|
||||||
@@ -69,10 +69,10 @@ def _get_provider_download_name(provider_type: str) -> str:
|
|||||||
platform_suffix = "macos-arm64"
|
platform_suffix = "macos-arm64"
|
||||||
else:
|
else:
|
||||||
platform_suffix = "macos-x64"
|
platform_suffix = "macos-x64"
|
||||||
ext = ""
|
ext = ".tar.gz"
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Provider downloads not supported on {system}")
|
raise ValueError(f"Provider downloads not supported on {system}")
|
||||||
|
|
||||||
return f"tts-provider-{provider_type}-{platform_suffix}{ext}"
|
return f"tts-provider-{provider_type}-{platform_suffix}{ext}"
|
||||||
|
|
||||||
|
|
||||||
@@ -84,97 +84,131 @@ def _get_provider_download_url(provider_type: str) -> str:
|
|||||||
|
|
||||||
async def download_provider(provider_type: str) -> Path:
|
async def download_provider(provider_type: str) -> Path:
|
||||||
"""
|
"""
|
||||||
Download a provider binary from Cloudflare R2.
|
Download and extract a provider archive from Cloudflare R2.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
provider_type: Type of provider to download (e.g., "pytorch-cpu")
|
provider_type: Type of provider to download (e.g., "pytorch-cpu")
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Path to the downloaded provider binary
|
Path to the extracted provider binary
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If provider_type is invalid
|
ValueError: If provider_type is invalid
|
||||||
httpx.HTTPError: If download fails
|
httpx.HTTPError: If download fails
|
||||||
"""
|
"""
|
||||||
if provider_type not in ["pytorch-cpu", "pytorch-cuda"]:
|
if provider_type not in ["pytorch-cpu", "pytorch-cuda"]:
|
||||||
raise ValueError(f"Provider type {provider_type} cannot be downloaded")
|
raise ValueError(f"Provider type {provider_type} cannot be downloaded")
|
||||||
|
|
||||||
progress_manager = get_progress_manager()
|
progress_manager = get_progress_manager()
|
||||||
task_manager = get_task_manager()
|
task_manager = get_task_manager()
|
||||||
|
|
||||||
binary_name = _get_provider_binary_name(provider_type)
|
archive_name = _get_provider_download_name(provider_type)
|
||||||
download_url = _get_provider_download_url(provider_type)
|
download_url = _get_provider_download_url(provider_type)
|
||||||
destination = _get_providers_dir() / binary_name
|
providers_dir = _get_providers_dir()
|
||||||
|
archive_path = providers_dir / archive_name
|
||||||
|
|
||||||
# Start tracking download
|
# Start tracking download
|
||||||
task_manager.start_download(provider_type)
|
task_manager.start_download(provider_type)
|
||||||
|
|
||||||
# Initialize progress state
|
# Initialize progress state
|
||||||
progress_manager.update_progress(
|
progress_manager.update_progress(
|
||||||
model_name=provider_type,
|
model_name=provider_type,
|
||||||
current=0,
|
current=0,
|
||||||
total=0, # Will be updated once we get Content-Length
|
total=0, # Will be updated once we get Content-Length
|
||||||
filename=binary_name,
|
filename=archive_name,
|
||||||
status="downloading",
|
status="downloading",
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Download archive
|
||||||
async with httpx.AsyncClient(timeout=300.0) as client:
|
async with httpx.AsyncClient(timeout=300.0) as client:
|
||||||
# First, get the file size
|
|
||||||
async with client.stream("GET", download_url) as response:
|
async with client.stream("GET", download_url) as response:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
# Get total size from Content-Length header
|
# Get total size from Content-Length header
|
||||||
total_size = int(response.headers.get("Content-Length", 0))
|
total_size = int(response.headers.get("Content-Length", 0))
|
||||||
|
|
||||||
if total_size > 0:
|
if total_size > 0:
|
||||||
progress_manager.update_progress(
|
progress_manager.update_progress(
|
||||||
model_name=provider_type,
|
model_name=provider_type,
|
||||||
current=0,
|
current=0,
|
||||||
total=total_size,
|
total=total_size,
|
||||||
filename=binary_name,
|
filename=archive_name,
|
||||||
status="downloading",
|
status="downloading",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Download with progress tracking
|
# Download with progress tracking
|
||||||
downloaded = 0
|
downloaded = 0
|
||||||
with open(destination, "wb") as f:
|
with open(archive_path, "wb") as f:
|
||||||
async for chunk in response.aiter_bytes(chunk_size=8192):
|
async for chunk in response.aiter_bytes(chunk_size=8192):
|
||||||
f.write(chunk)
|
f.write(chunk)
|
||||||
downloaded += len(chunk)
|
downloaded += len(chunk)
|
||||||
|
|
||||||
# Update progress
|
# Update progress
|
||||||
progress_manager.update_progress(
|
progress_manager.update_progress(
|
||||||
model_name=provider_type,
|
model_name=provider_type,
|
||||||
current=downloaded,
|
current=downloaded,
|
||||||
total=total_size if total_size > 0 else downloaded,
|
total=total_size if total_size > 0 else downloaded,
|
||||||
filename=binary_name,
|
filename=archive_name,
|
||||||
status="downloading",
|
status="downloading",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Extract archive
|
||||||
|
progress_manager.update_progress(
|
||||||
|
model_name=provider_type,
|
||||||
|
current=downloaded,
|
||||||
|
total=downloaded,
|
||||||
|
filename="Extracting...",
|
||||||
|
status="downloading",
|
||||||
|
)
|
||||||
|
|
||||||
|
import zipfile
|
||||||
|
import tarfile
|
||||||
|
|
||||||
|
if archive_name.endswith('.zip'):
|
||||||
|
with zipfile.ZipFile(archive_path, 'r') as zip_ref:
|
||||||
|
zip_ref.extractall(providers_dir)
|
||||||
|
elif archive_name.endswith('.tar.gz'):
|
||||||
|
with tarfile.open(archive_path, 'r:gz') as tar_ref:
|
||||||
|
tar_ref.extractall(providers_dir)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported archive format: {archive_name}")
|
||||||
|
|
||||||
|
# Remove archive after extraction
|
||||||
|
archive_path.unlink()
|
||||||
|
|
||||||
|
# Get path to extracted binary
|
||||||
|
binary_path = get_provider_binary_path(provider_type)
|
||||||
|
if not binary_path:
|
||||||
|
raise ValueError(f"Provider binary not found after extraction")
|
||||||
|
|
||||||
|
# Make executable on Unix systems
|
||||||
|
if platform.system() != "Windows":
|
||||||
|
binary_path.chmod(0o755)
|
||||||
|
|
||||||
# Mark as complete
|
# Mark as complete
|
||||||
progress_manager.update_progress(
|
progress_manager.update_progress(
|
||||||
model_name=provider_type,
|
model_name=provider_type,
|
||||||
current=downloaded,
|
current=downloaded,
|
||||||
total=downloaded,
|
total=downloaded,
|
||||||
filename=binary_name,
|
filename=_get_provider_binary_name(provider_type),
|
||||||
status="complete",
|
status="complete",
|
||||||
)
|
)
|
||||||
task_manager.complete_download(provider_type)
|
task_manager.complete_download(provider_type)
|
||||||
|
|
||||||
# Make executable on Unix systems
|
return binary_path
|
||||||
if platform.system() != "Windows":
|
|
||||||
destination.chmod(0o755)
|
|
||||||
|
|
||||||
return destination
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# Clean up archive if it exists
|
||||||
|
if archive_path.exists():
|
||||||
|
archive_path.unlink()
|
||||||
|
|
||||||
# Mark as error
|
# Mark as error
|
||||||
progress_manager.update_progress(
|
progress_manager.update_progress(
|
||||||
model_name=provider_type,
|
model_name=provider_type,
|
||||||
current=0,
|
current=0,
|
||||||
total=0,
|
total=0,
|
||||||
filename=binary_name,
|
filename=archive_name,
|
||||||
status="error",
|
status="error",
|
||||||
)
|
)
|
||||||
task_manager.error_download(provider_type, str(e))
|
task_manager.error_download(provider_type, str(e))
|
||||||
@@ -184,19 +218,28 @@ async def download_provider(provider_type: str) -> Path:
|
|||||||
def get_provider_binary_path(provider_type: str) -> Optional[Path]:
|
def get_provider_binary_path(provider_type: str) -> Optional[Path]:
|
||||||
"""
|
"""
|
||||||
Get the path to an installed provider binary.
|
Get the path to an installed provider binary.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
provider_type: Type of provider
|
provider_type: Type of provider
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Path to provider binary, or None if not installed
|
Path to provider binary, or None if not installed
|
||||||
"""
|
"""
|
||||||
|
providers_dir = _get_providers_dir()
|
||||||
binary_name = _get_provider_binary_name(provider_type)
|
binary_name = _get_provider_binary_name(provider_type)
|
||||||
provider_path = _get_providers_dir() / binary_name
|
|
||||||
|
# Check for --onedir structure (directory with binary inside)
|
||||||
|
provider_dir = providers_dir / f"tts-provider-{provider_type}"
|
||||||
|
if provider_dir.exists() and provider_dir.is_dir():
|
||||||
|
binary_path = provider_dir / binary_name
|
||||||
|
if binary_path.exists() and binary_path.is_file():
|
||||||
|
return binary_path
|
||||||
|
|
||||||
|
# Fallback: check for direct binary (legacy)
|
||||||
|
provider_path = providers_dir / binary_name
|
||||||
if provider_path.exists() and provider_path.is_file():
|
if provider_path.exists() and provider_path.is_file():
|
||||||
return provider_path
|
return provider_path
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
# User data directory
|
|
||||||
# This directory contains:
|
|
||||||
# - profiles/ - Voice profile audio files
|
|
||||||
# - generations/ - Generated audio files
|
|
||||||
# - projects/ - Audio studio project files
|
|
||||||
# - voicebox.db - SQLite database
|
|
||||||
# - cache/ - Voice prompt cache files
|
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
# Voice prompt cache files
|
|
||||||
@@ -16,7 +16,7 @@ def build_provider():
|
|||||||
# PyInstaller arguments
|
# PyInstaller arguments
|
||||||
args = [
|
args = [
|
||||||
'main.py',
|
'main.py',
|
||||||
'--onefile',
|
'--onedir', # Changed from --onefile to work around Windows extraction issues
|
||||||
'--name', 'tts-provider-pytorch-cpu',
|
'--name', 'tts-provider-pytorch-cpu',
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -51,6 +51,18 @@ def build_provider():
|
|||||||
'--collect-submodules', 'jaraco',
|
'--collect-submodules', 'jaraco',
|
||||||
'--hidden-import', 'fastapi',
|
'--hidden-import', 'fastapi',
|
||||||
'--hidden-import', 'uvicorn',
|
'--hidden-import', 'uvicorn',
|
||||||
|
# Critical uvicorn imports for PyInstaller
|
||||||
|
'--hidden-import', 'uvicorn.logging',
|
||||||
|
'--hidden-import', 'uvicorn.loops',
|
||||||
|
'--hidden-import', 'uvicorn.loops.auto',
|
||||||
|
'--hidden-import', 'uvicorn.protocols',
|
||||||
|
'--hidden-import', 'uvicorn.protocols.http',
|
||||||
|
'--hidden-import', 'uvicorn.protocols.http.auto',
|
||||||
|
'--hidden-import', 'uvicorn.protocols.websockets',
|
||||||
|
'--hidden-import', 'uvicorn.protocols.websockets.auto',
|
||||||
|
'--hidden-import', 'uvicorn.lifespan',
|
||||||
|
'--hidden-import', 'uvicorn.lifespan.on',
|
||||||
|
'--collect-submodules', 'uvicorn',
|
||||||
'--hidden-import', 'soundfile',
|
'--hidden-import', 'soundfile',
|
||||||
'--hidden-import', 'numpy',
|
'--hidden-import', 'numpy',
|
||||||
'--hidden-import', 'librosa',
|
'--hidden-import', 'librosa',
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ def build_provider():
|
|||||||
# PyInstaller arguments
|
# PyInstaller arguments
|
||||||
args = [
|
args = [
|
||||||
'main.py',
|
'main.py',
|
||||||
'--onefile',
|
'--onedir', # Changed from --onefile to work around Windows extraction issues
|
||||||
'--name', 'tts-provider-pytorch-cuda',
|
'--name', 'tts-provider-pytorch-cuda',
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -53,6 +53,18 @@ def build_provider():
|
|||||||
'--collect-submodules', 'jaraco',
|
'--collect-submodules', 'jaraco',
|
||||||
'--hidden-import', 'fastapi',
|
'--hidden-import', 'fastapi',
|
||||||
'--hidden-import', 'uvicorn',
|
'--hidden-import', 'uvicorn',
|
||||||
|
# Critical uvicorn imports for PyInstaller
|
||||||
|
'--hidden-import', 'uvicorn.logging',
|
||||||
|
'--hidden-import', 'uvicorn.loops',
|
||||||
|
'--hidden-import', 'uvicorn.loops.auto',
|
||||||
|
'--hidden-import', 'uvicorn.protocols',
|
||||||
|
'--hidden-import', 'uvicorn.protocols.http',
|
||||||
|
'--hidden-import', 'uvicorn.protocols.http.auto',
|
||||||
|
'--hidden-import', 'uvicorn.protocols.websockets',
|
||||||
|
'--hidden-import', 'uvicorn.protocols.websockets.auto',
|
||||||
|
'--hidden-import', 'uvicorn.lifespan',
|
||||||
|
'--hidden-import', 'uvicorn.lifespan.on',
|
||||||
|
'--collect-submodules', 'uvicorn',
|
||||||
'--hidden-import', 'soundfile',
|
'--hidden-import', 'soundfile',
|
||||||
'--hidden-import', 'numpy',
|
'--hidden-import', 'numpy',
|
||||||
'--hidden-import', 'librosa',
|
'--hidden-import', 'librosa',
|
||||||
|
|||||||
Reference in New Issue
Block a user