Enhance release workflow and update provider settings

- Added macOS support for PyTorch CPU providers in the release workflow.
- Updated the ProviderSettings component to handle macOS-specific conditions and improve UI interactions.
- Refactored the radio group component styles for better accessibility and visual consistency.
- Improved provider management logic to ensure proper handling of available providers across different platforms.
This commit is contained in:
Jamie Pine
2026-02-01 00:00:47 -08:00
parent 6dd5bb2311
commit 3b14f81741
6 changed files with 173 additions and 182 deletions
+15 -3
View File
@@ -28,13 +28,21 @@ jobs:
provider: "pytorch-cuda" provider: "pytorch-cuda"
python-version: "3.12" python-version: "3.12"
# PyTorch CPU provider (Linux) # PyTorch CPU provider (Linux)
# - platform: "ubuntu-22.04" - platform: "ubuntu-22.04"
# provider: "pytorch-cpu" provider: "pytorch-cpu"
# python-version: "3.12" python-version: "3.12"
# PyTorch CUDA provider (Linux) - large binary, uploaded to R2 # PyTorch CUDA provider (Linux) - large binary, uploaded to R2
- platform: "ubuntu-22.04" - platform: "ubuntu-22.04"
provider: "pytorch-cuda" provider: "pytorch-cuda"
python-version: "3.12" python-version: "3.12"
# PyTorch CPU provider (macOS Apple Silicon)
- platform: "macos-latest"
provider: "pytorch-cpu"
python-version: "3.12"
# PyTorch CPU provider (macOS Intel)
- platform: "macos-15-intel"
provider: "pytorch-cpu"
python-version: "3.12"
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -101,6 +109,10 @@ jobs:
# Add platform suffix for clarity # Add platform suffix for clarity
if [ "${{ matrix.platform }}" == "windows-latest" ]; then if [ "${{ matrix.platform }}" == "windows-latest" ]; then
UPLOAD_NAME="tts-provider-${{ matrix.provider }}-windows.exe" UPLOAD_NAME="tts-provider-${{ matrix.provider }}-windows.exe"
elif [ "${{ matrix.platform }}" == "macos-latest" ]; then
UPLOAD_NAME="tts-provider-${{ matrix.provider }}-macos-arm64"
elif [ "${{ matrix.platform }}" == "macos-15-intel" ]; then
UPLOAD_NAME="tts-provider-${{ matrix.provider }}-macos-x64"
else else
UPLOAD_NAME="tts-provider-${{ matrix.provider }}-linux" UPLOAD_NAME="tts-provider-${{ matrix.provider }}-linux"
fi fi
@@ -1,6 +1,6 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Delete01Icon, Download01Icon, Loading01Icon } from '@hugeicons/core-free-icons';
import { HugeiconsIcon } from '@hugeicons/react'; import { HugeiconsIcon } from '@hugeicons/react';
import { Download01Icon, Loading01Icon, Delete01Icon } from '@hugeicons/core-free-icons'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { import {
AlertDialog, AlertDialog,
@@ -23,12 +23,18 @@ import { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast';
const isMacOS = () => navigator.platform.toLowerCase().includes('mac'); const isMacOS = () => navigator.platform.toLowerCase().includes('mac');
type ProviderType = 'auto' | 'bundled-mlx' | 'bundled-pytorch' | 'pytorch-cpu' | 'pytorch-cuda' | 'remote' | 'openai'; type ProviderType =
| 'auto'
| 'bundled-mlx'
| 'bundled-pytorch'
| 'pytorch-cpu'
| 'pytorch-cuda'
| 'remote'
| 'openai';
export function ProviderSettings() { export function ProviderSettings() {
const { toast } = useToast(); const { toast } = useToast();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [selectedProvider, setSelectedProvider] = useState<ProviderType>('auto');
const [downloadingProvider, setDownloadingProvider] = useState<string | null>(null); const [downloadingProvider, setDownloadingProvider] = useState<string | null>(null);
const { data: providersData, isLoading } = useQuery({ const { data: providersData, isLoading } = useQuery({
@@ -167,6 +173,7 @@ export function ProviderSettings() {
// Determine current active provider // Determine current active provider
const currentProvider = activeProvider?.provider || 'auto'; const currentProvider = activeProvider?.provider || 'auto';
const selectedProvider = currentProvider as ProviderType;
return ( return (
<> <>
@@ -176,41 +183,25 @@ export function ProviderSettings() {
<CardDescription>Choose how Voicebox generates speech</CardDescription> <CardDescription>Choose how Voicebox generates speech</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<RadioGroup <RadioGroup value={selectedProvider} onValueChange={(value) => handleStart(value)}>
value={selectedProvider}
onValueChange={(value) => setSelectedProvider(value as ProviderType)}
>
{/* Auto-detect */}
<div className="flex items-center space-x-2 py-2">
<RadioGroupItem value="auto" id="auto" />
<Label htmlFor="auto" className="flex-1 cursor-pointer">
<div className="font-medium">Auto-detect (Recommended)</div>
<div className="text-sm text-muted-foreground">
Automatically choose the best available provider
</div>
</Label>
{currentProvider === 'auto' && (
<Badge variant="outline" className="ml-2">
Active
</Badge>
)}
</div>
{/* PyTorch CUDA */} {/* PyTorch CUDA */}
<div className="flex items-center justify-between py-2"> <div
<div className="flex items-center space-x-2 flex-1"> className={`flex items-center justify-between py-2 ${isMacOS() ? 'opacity-50' : ''}`}
<RadioGroupItem value="pytorch-cuda" id="cuda" /> >
<Label htmlFor="cuda" className="flex-1 cursor-pointer"> <div className="flex items-center space-x-3 flex-1">
<RadioGroupItem value="pytorch-cuda" id="cuda" disabled={isMacOS()} />
<Label
htmlFor="cuda"
className={`flex-1 ${isMacOS() ? 'cursor-not-allowed' : 'cursor-pointer'}`}
>
<div className="font-medium">PyTorch CUDA (NVIDIA GPU)</div> <div className="font-medium">PyTorch CUDA (NVIDIA GPU)</div>
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
4-5x faster inference on NVIDIA GPUs {isMacOS() ? 'Not available on macOS' : '4-5x faster inference on NVIDIA GPUs'}
</div> </div>
</Label> </Label>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{currentProvider === 'pytorch-cuda' && ( {currentProvider === 'pytorch-cuda' && <Badge variant="outline">Active</Badge>}
<Badge variant="outline">Active</Badge>
)}
{!installedProviders.includes('pytorch-cuda') && ( {!installedProviders.includes('pytorch-cuda') && (
<Button <Button
onClick={() => handleDownload('pytorch-cuda')} onClick={() => handleDownload('pytorch-cuda')}
@@ -218,7 +209,11 @@ export function ProviderSettings() {
disabled={downloadingProvider === 'pytorch-cuda'} disabled={downloadingProvider === 'pytorch-cuda'}
> >
{downloadingProvider === 'pytorch-cuda' ? ( {downloadingProvider === 'pytorch-cuda' ? (
<HugeiconsIcon icon={Loading01Icon} size={16} className="h-4 w-4 animate-spin" /> <HugeiconsIcon
icon={Loading01Icon}
size={16}
className="h-4 w-4 animate-spin"
/>
) : ( ) : (
<> <>
<HugeiconsIcon icon={Download01Icon} size={16} className="h-4 w-4 mr-1" /> <HugeiconsIcon icon={Download01Icon} size={16} className="h-4 w-4 mr-1" />
@@ -227,148 +222,109 @@ export function ProviderSettings() {
)} )}
</Button> </Button>
)} )}
{installedProviders.includes('pytorch-cuda') && selectedProvider !== 'pytorch-cuda' && ( {installedProviders.includes('pytorch-cuda') &&
<Button selectedProvider !== 'pytorch-cuda' && (
onClick={() => handleStart('pytorch-cuda')} <Button onClick={() => handleStart('pytorch-cuda')} size="sm" variant="outline">
size="sm" Start
variant="outline" </Button>
> )}
Start
</Button>
)}
{installedProviders.includes('pytorch-cuda') && ( {installedProviders.includes('pytorch-cuda') && (
<Button <Button onClick={() => handleDelete('pytorch-cuda')} size="sm" variant="ghost">
onClick={() => handleDelete('pytorch-cuda')}
size="sm"
variant="ghost"
>
<HugeiconsIcon icon={Delete01Icon} size={16} className="h-4 w-4" /> <HugeiconsIcon icon={Delete01Icon} size={16} className="h-4 w-4" />
</Button> </Button>
)} )}
</div> </div>
</div> </div>
{/* PyTorch CPU (Windows/Linux only) */} {/* PyTorch CPU */}
{!isMacOS() && ( <div className="flex items-center justify-between py-2">
<div className="flex items-center justify-between py-2"> <div className="flex items-center space-x-3 flex-1">
<div className="flex items-center space-x-2 flex-1"> <RadioGroupItem value="pytorch-cpu" id="cpu" />
<RadioGroupItem value="pytorch-cpu" id="cpu" /> <Label htmlFor="cpu" className="flex-1 cursor-pointer">
<Label htmlFor="cpu" className="flex-1 cursor-pointer"> <div className="font-medium">PyTorch CPU</div>
<div className="font-medium">PyTorch CPU</div> <div className="text-sm text-muted-foreground">
<div className="text-sm text-muted-foreground"> Works on any system, slower inference
Works on any system, slower inference </div>
</div> </Label>
</Label> </div>
</div> <div className="flex items-center gap-2">
<div className="flex items-center gap-2"> {currentProvider === 'pytorch-cpu' && <Badge variant="outline">Active</Badge>}
{currentProvider === 'pytorch-cpu' && ( {!installedProviders.includes('pytorch-cpu') && (
<Badge variant="outline">Active</Badge> <Button
)} onClick={() => handleDownload('pytorch-cpu')}
{!installedProviders.includes('pytorch-cpu') && ( size="sm"
<Button disabled={downloadingProvider === 'pytorch-cpu'}
onClick={() => handleDownload('pytorch-cpu')} >
size="sm" {downloadingProvider === 'pytorch-cpu' ? (
disabled={downloadingProvider === 'pytorch-cpu'} <HugeiconsIcon
> icon={Loading01Icon}
{downloadingProvider === 'pytorch-cpu' ? ( size={16}
<HugeiconsIcon icon={Loading01Icon} size={16} className="h-4 w-4 animate-spin" /> className="h-4 w-4 animate-spin"
) : ( />
<> ) : (
<HugeiconsIcon icon={Download01Icon} size={16} className="h-4 w-4 mr-1" /> <>
Download (300MB) <HugeiconsIcon icon={Download01Icon} size={16} className="h-4 w-4 mr-1" />
</> Download (300MB)
)} </>
</Button> )}
)} </Button>
{installedProviders.includes('pytorch-cpu') && selectedProvider !== 'pytorch-cpu' && ( )}
<Button {installedProviders.includes('pytorch-cpu') &&
onClick={() => handleStart('pytorch-cpu')} selectedProvider !== 'pytorch-cpu' && (
size="sm" <Button onClick={() => handleStart('pytorch-cpu')} size="sm" variant="outline">
variant="outline"
>
Start Start
</Button> </Button>
)} )}
{installedProviders.includes('pytorch-cpu') && ( {installedProviders.includes('pytorch-cpu') && (
<Button <Button onClick={() => handleDelete('pytorch-cpu')} size="sm" variant="ghost">
onClick={() => handleDelete('pytorch-cpu')} <HugeiconsIcon icon={Delete01Icon} size={16} className="h-4 w-4" />
size="sm" </Button>
variant="ghost" )}
>
<HugeiconsIcon icon={Delete01Icon} size={16} className="h-4 w-4" />
</Button>
)}
</div>
</div> </div>
)} </div>
{/* MLX bundled (macOS only) */} {/* MLX bundled (macOS Apple Silicon only) */}
{isMacOS() && ( {isMacOS() && (
<div className="p-3 bg-muted rounded-md"> <div className="flex items-center space-x-3 py-2">
<div className="text-sm"> <RadioGroupItem value="bundled-mlx" id="mlx" />
<div className="font-medium flex items-center gap-2"> <Label htmlFor="mlx" className="flex-1 cursor-pointer">
MLX (Apple Silicon) <div className="font-medium">MLX (Apple Silicon)</div>
{currentProvider === 'bundled-mlx' && ( <div className="text-sm text-muted-foreground">
<Badge variant="outline">Active</Badge> Bundled with the app - optimized for M-series chips
)}
</div> </div>
<div className="text-muted-foreground mt-1"> </Label>
Bundled with the app - optimized for M1/M2/M3 chips {currentProvider === 'bundled-mlx' && (
</div> <Badge variant="outline" className="ml-2">
</div> Active
</Badge>
)}
</div> </div>
)} )}
{/* Remote */} {/* Remote */}
<div className="space-y-2 py-2"> <div className="space-y-2 py-2 opacity-50">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-3">
<RadioGroupItem value="remote" id="remote" /> <RadioGroupItem value="remote" id="remote" disabled />
<Label htmlFor="remote" className="flex-1 cursor-pointer"> <Label htmlFor="remote" className="flex-1 cursor-not-allowed">
<div className="font-medium">Remote Server</div> <div className="font-medium">Remote Server</div>
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
Connect to your own TTS server Connect to your own TTS server (coming soon)
</div> </div>
</Label> </Label>
</div> </div>
{selectedProvider === 'remote' && (
<div className="ml-6">
<input
type="text"
placeholder="http://your-server:8000"
className="w-full px-3 py-2 border rounded-md"
disabled
/>
<div className="text-xs text-muted-foreground mt-1">
Remote provider support coming soon
</div>
</div>
)}
</div> </div>
{/* OpenAI */} {/* OpenAI */}
<div className="space-y-2 py-2"> <div className="space-y-2 py-2 opacity-50">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-3">
<RadioGroupItem value="openai" id="openai" /> <RadioGroupItem value="openai" id="openai" disabled />
<Label htmlFor="openai" className="flex-1 cursor-pointer"> <Label htmlFor="openai" className="flex-1 cursor-not-allowed">
<div className="font-medium">OpenAI API</div> <div className="font-medium">OpenAI API</div>
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
Use OpenAI's TTS API (requires API key) Use OpenAI's TTS API (coming soon)
</div> </div>
</Label> </Label>
</div> </div>
{selectedProvider === 'openai' && (
<div className="ml-6">
<input
type="password"
placeholder="sk-..."
className="w-full px-3 py-2 border rounded-md"
disabled
/>
<div className="text-xs text-muted-foreground mt-1">
OpenAI provider support coming soon
</div>
</div>
)}
</div> </div>
</RadioGroup> </RadioGroup>
</CardContent> </CardContent>
@@ -385,7 +341,10 @@ export function ProviderSettings() {
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={confirmDelete} className="bg-destructive text-destructive-foreground"> <AlertDialogAction
onClick={confirmDelete}
className="bg-destructive text-destructive-foreground"
>
Delete Delete
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
+1 -1
View File
@@ -29,7 +29,7 @@ const RadioGroupItem = React.forwardRef<
<RadioGroupPrimitive.Item <RadioGroupPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", "aspect-square h-4 w-4 rounded-full border border-accent text-accent ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className className
)} )}
{...props} {...props}
+45 -33
View File
@@ -10,7 +10,7 @@ from .base import TTSProvider
from .types import ProviderType from .types import ProviderType
from .bundled import BundledProvider from .bundled import BundledProvider
from .local import LocalProvider from .local import LocalProvider
from .installer import get_provider_binary_path from .installer import get_provider_binary_path, _get_providers_dir
from ..config import get_data_dir from ..config import get_data_dir
import subprocess import subprocess
import socket import socket
@@ -50,38 +50,44 @@ class ProviderManager:
Args: Args:
provider_type: Type of provider to start provider_type: Type of provider to start
""" """
if provider_type in ["bundled-mlx", "bundled-pytorch"]: if provider_type == "bundled-mlx":
# Use bundled provider # Use bundled MLX provider
self.active_provider = self._get_default_provider() self.active_provider = self._get_default_provider()
elif provider_type in ["pytorch-cpu", "pytorch-cuda"]: elif provider_type in ["pytorch-cpu", "pytorch-cuda"]:
# Start local provider subprocess # Try to start external provider subprocess if binary exists
provider_path = get_provider_binary_path(provider_type) provider_path = get_provider_binary_path(provider_type)
if not provider_path or not provider_path.exists(): if provider_path and provider_path.exists():
raise ValueError(f"Provider {provider_type} is not installed. Please download it first.") # External downloaded provider exists, start it
# Find a free port
# Find a free port port = self._get_free_port()
port = self._get_free_port()
# Start provider subprocess
# Start provider subprocess from ..config import get_data_dir
from ..config import get_data_dir process = subprocess.Popen(
process = subprocess.Popen( [
[ str(provider_path),
str(provider_path), "--port", str(port),
"--port", str(port), "--data-dir", str(get_data_dir()),
"--data-dir", str(get_data_dir()), ],
], stdout=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
stderr=subprocess.PIPE, )
)
# Wait for provider to be ready
# Wait for provider to be ready base_url = f"http://127.0.0.1:{port}"
base_url = f"http://127.0.0.1:{port}" await self._wait_for_provider_health(base_url, timeout=30)
await self._wait_for_provider_health(base_url, timeout=30)
# Create LocalProvider instance
# Create LocalProvider instance self.active_provider = LocalProvider(base_url)
self.active_provider = LocalProvider(base_url) self._provider_process = process
self._provider_process = process self._provider_port = port
self._provider_port = port else:
# No external binary, use bundled provider (if available)
if provider_type == "pytorch-cpu":
# PyTorch CPU can use bundled backend
self.active_provider = self._get_default_provider()
else:
raise ValueError(f"Provider {provider_type} is not installed. Please download it first.")
elif provider_type == "remote": elif provider_type == "remote":
# Remote provider - will be implemented in Phase 5 # Remote provider - will be implemented in Phase 5
raise NotImplementedError("Remote provider not yet implemented") raise NotImplementedError("Remote provider not yet implemented")
@@ -122,11 +128,17 @@ class ProviderManager:
# Bundled providers are always available # Bundled providers are always available
system = platform.system() system = platform.system()
machine = platform.machine() machine = platform.machine()
if system == "Darwin" and machine == "arm64": if system == "Darwin" and machine == "arm64":
# Apple Silicon gets MLX
installed.append("bundled-mlx") installed.append("bundled-mlx")
else:
installed.append("bundled-pytorch") # PyTorch CPU is available on all platforms (check if bundled or downloaded)
# For now, assume it's bundled on macOS Intel, Windows, Linux
# Downloaded binaries will be detected below
if not (system == "Darwin" and machine == "arm64"):
# Non-Apple Silicon systems have PyTorch CPU bundled
installed.append("pytorch-cpu")
# Check for downloaded providers (Phase 2) # Check for downloaded providers (Phase 2)
providers_dir = _get_providers_dir() providers_dir = _get_providers_dir()
+8
View File
@@ -62,6 +62,14 @@ def _get_provider_download_name(provider_type: str) -> str:
elif system == "Linux": elif system == "Linux":
platform_suffix = "linux" platform_suffix = "linux"
ext = "" ext = ""
elif system == "Darwin":
# Detect macOS architecture
machine = platform.machine()
if machine == "arm64":
platform_suffix = "macos-arm64"
else:
platform_suffix = "macos-x64"
ext = ""
else: else:
raise ValueError(f"Provider downloads not supported on {system}") raise ValueError(f"Provider downloads not supported on {system}")
Binary file not shown.