Enhance README and UI Components for Performance and Features

- Updated README.md to highlight MLX backend performance improvements on Mac with Metal acceleration.
- Refined ProfileCard and ProfileForm components by optimizing imports and improving error handling for avatar uploads.
- Adjusted landing page content to better describe features, including a new multi-voice narrative editor and performance optimizations for different platforms.
- Bumped version to 0.1.11 in Cargo.lock to reflect recent changes.
This commit is contained in:
Jamie Pine
2026-01-30 02:53:15 -08:00
parent bef4092e6e
commit 1b66a528d1
7 changed files with 38 additions and 33 deletions
+2
View File
@@ -68,6 +68,7 @@ Unlike cloud services that lock your voice data behind subscriptions, Voicebox g
- **Model flexibility** — currently powered by Qwen3-TTS, with support for XTTS, Bark, and other models coming soon
- **API-first** — use the desktop app or integrate voice synthesis into your own projects
- **Native performance** — built with Tauri (Rust), not Electron
- **Super fast on Mac** — MLX backend with native Metal acceleration for 4-5x faster inference on Apple Silicon
Download a voice model, clone any voice from a few seconds of audio, and compose multi-voice projects with studio-grade editing tools. No Python install required, no cloud dependency, no limits.
@@ -97,6 +98,7 @@ Powered by Alibaba's **Qwen3-TTS** — a breakthrough model that achieves near-p
- **Instant cloning** — Upload a sample, get a voice profile
- **High fidelity** — Natural prosody, emotion, and cadence
- **Multi-language** — English, Chinese, and more coming
- **Lightning fast on Mac** — MLX backend leverages Apple Silicon's Neural Engine for super fast generation
### Voice Profile Management
@@ -1,6 +1,5 @@
import { Download, Edit, Mic, Trash2 } from 'lucide-react';
import { useState } from 'react';
import { useServerStore } from '@/stores/serverStore';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@@ -16,6 +15,7 @@ import {
import type { VoiceProfileResponse } from '@/lib/api/types';
import { useDeleteProfile, useExportProfile } from '@/lib/hooks/useProfiles';
import { cn } from '@/lib/utils/cn';
import { useServerStore } from '@/stores/serverStore';
import { useUIStore } from '@/stores/uiStore';
interface ProfileCardProps {
@@ -35,9 +35,7 @@ export function ProfileCard({ profile }: ProfileCardProps) {
const isSelected = selectedProfileId === profile.id;
const avatarUrl = profile.avatar_path
? `${serverUrl}/profiles/${profile.id}/avatar`
: null;
const avatarUrl = profile.avatar_path ? `${serverUrl}/profiles/${profile.id}/avatar` : null;
const handleSelect = () => {
setSelectedProfileId(isSelected ? null : profile.id);
@@ -81,7 +79,7 @@ export function ProfileCard({ profile }: ProfileCardProps) {
alt={`${profile.name} avatar`}
className={cn(
'h-full w-full object-cover transition-all duration-200',
!isSelected && 'grayscale'
!isSelected && 'grayscale',
)}
onError={() => setAvatarError(true)}
/>
@@ -45,8 +45,8 @@ import { useSystemAudioCapture } from '@/lib/hooks/useSystemAudioCapture';
import { useTranscription } from '@/lib/hooks/useTranscription';
import { isTauri } from '@/lib/tauri';
import { formatAudioDuration, getAudioDuration } from '@/lib/utils/audio';
import { type ProfileFormDraft, useUIStore } from '@/stores/uiStore';
import { useServerStore } from '@/stores/serverStore';
import { type ProfileFormDraft, useUIStore } from '@/stores/uiStore';
import { AudioSampleRecording } from './AudioSampleRecording';
import { AudioSampleSystem } from './AudioSampleSystem';
import { AudioSampleUpload } from './AudioSampleUpload';
@@ -427,7 +427,8 @@ export function ProfileForm() {
} catch (avatarError) {
toast({
title: 'Avatar upload failed',
description: avatarError instanceof Error ? avatarError.message : 'Failed to upload avatar',
description:
avatarError instanceof Error ? avatarError.message : 'Failed to upload avatar',
variant: 'destructive',
});
}
@@ -520,7 +521,8 @@ export function ProfileForm() {
} catch (avatarError) {
toast({
title: 'Avatar upload failed',
description: avatarError instanceof Error ? avatarError.message : 'Failed to upload avatar',
description:
avatarError instanceof Error ? avatarError.message : 'Failed to upload avatar',
variant: 'destructive',
});
}
+15 -14
View File
@@ -424,34 +424,35 @@ class MLXSTTBackend:
) -> str:
"""
Transcribe audio to text.
Args:
audio_path: Path to audio file
language: Optional language hint (en or zh)
Returns:
Transcribed text
"""
await self.load_model_async(None)
def _transcribe_sync():
"""Run synchronous transcription in thread pool."""
# Load audio
audio, sr = load_audio(audio_path, sample_rate=16000)
# MLX Whisper transcription
# The API may vary - check mlx-audio documentation
# For now, assuming similar API to PyTorch Whisper
result = self.model.transcribe(audio, language=language)
# Extract text from result (format may vary)
# MLX Whisper transcription using generate method
# The generate method accepts audio path directly
decode_options = {}
if language:
decode_options["language"] = language
result = self.model.generate(str(audio_path), **decode_options)
# Extract text from result
if isinstance(result, str):
return result.strip()
elif isinstance(result, dict):
return result.get("text", "").strip()
elif hasattr(result, "text"):
return result.text.strip()
else:
# Try to get text attribute
return str(result).strip()
# Run blocking transcription in thread pool
return await asyncio.to_thread(_transcribe_sync)
+12 -10
View File
@@ -5,7 +5,7 @@ import Image from 'next/image';
import { useEffect, useState } from 'react';
import { AppleIcon, LinuxIcon, WindowsIcon } from '@/components/PlatformIcons';
import { Button } from '@/components/ui/button';
import { Section, SectionTitle } from '@/components/ui/section';
import { Section } from '@/components/ui/section';
import { DOWNLOAD_LINKS, GITHUB_REPO } from '@/lib/constants';
import type { DownloadLinks } from '@/lib/releases';
import { FeatureCard } from '../components/ui/feature-card';
@@ -39,17 +39,19 @@ export default function Home() {
"Powered by Alibaba's Qwen3-TTS model for exceptional voice quality and accuracy.",
icon: <Zap className="h-6 w-6" />,
},
{
title: 'Stories Editor',
description:
'Create multi-voice narratives with a timeline-based editor. Arrange tracks, trim clips, and mix conversations.',
icon: <Code className="h-6 w-6" />,
},
{
title: 'Multi-Sample Support',
description:
'Combine multiple voice samples for higher quality and more natural-sounding results.',
icon: <Code className="h-6 w-6" />,
},
{
title: 'Smart Caching',
description: 'Instant re-generation with voice prompt caching. No need to reprocess samples.',
icon: <Zap className="h-6 w-6" />,
},
{
title: 'Local or Remote',
description:
@@ -246,6 +248,10 @@ export default function Home() {
model, clone any voice from a few seconds of audio, and compose multi-voice projects
with studio-grade editing tools.
</p>
<p>
Optimized for performance with <strong>Metal acceleration on Mac</strong> and{' '}
<strong>CUDA acceleration on Windows/Linux</strong> for fast, local inference.
</p>
<p className="text-foreground/60">No Python install required.</p>
</div>
</div>
@@ -281,10 +287,6 @@ export default function Home() {
{/* Features Section */}
<Section id="features">
<SectionTitle className="mb-4 text-center">Features</SectionTitle>
<p className="text-sm text-muted-foreground mb-8 text-center max-w-2xl mx-auto">
Everything you need for professional voice cloning in a desktop app.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
{features.map((feature) => (
<FeatureCard
+1 -1
View File
@@ -5041,7 +5041,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "voicebox"
version = "0.1.10"
version = "0.1.11"
dependencies = [
"base64 0.22.1",
"core-foundation-sys",
Binary file not shown.