mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-10-03 00:55:14 -07:00
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:
@@ -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
|
- **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
|
- **API-first** — use the desktop app or integrate voice synthesis into your own projects
|
||||||
- **Native performance** — built with Tauri (Rust), not Electron
|
- **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.
|
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
|
- **Instant cloning** — Upload a sample, get a voice profile
|
||||||
- **High fidelity** — Natural prosody, emotion, and cadence
|
- **High fidelity** — Natural prosody, emotion, and cadence
|
||||||
- **Multi-language** — English, Chinese, and more coming
|
- **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
|
### Voice Profile Management
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Download, Edit, Mic, Trash2 } from 'lucide-react';
|
import { Download, Edit, Mic, Trash2 } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useServerStore } from '@/stores/serverStore';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
@@ -16,6 +15,7 @@ import {
|
|||||||
import type { VoiceProfileResponse } from '@/lib/api/types';
|
import type { VoiceProfileResponse } from '@/lib/api/types';
|
||||||
import { useDeleteProfile, useExportProfile } from '@/lib/hooks/useProfiles';
|
import { useDeleteProfile, useExportProfile } from '@/lib/hooks/useProfiles';
|
||||||
import { cn } from '@/lib/utils/cn';
|
import { cn } from '@/lib/utils/cn';
|
||||||
|
import { useServerStore } from '@/stores/serverStore';
|
||||||
import { useUIStore } from '@/stores/uiStore';
|
import { useUIStore } from '@/stores/uiStore';
|
||||||
|
|
||||||
interface ProfileCardProps {
|
interface ProfileCardProps {
|
||||||
@@ -35,9 +35,7 @@ export function ProfileCard({ profile }: ProfileCardProps) {
|
|||||||
|
|
||||||
const isSelected = selectedProfileId === profile.id;
|
const isSelected = selectedProfileId === profile.id;
|
||||||
|
|
||||||
const avatarUrl = profile.avatar_path
|
const avatarUrl = profile.avatar_path ? `${serverUrl}/profiles/${profile.id}/avatar` : null;
|
||||||
? `${serverUrl}/profiles/${profile.id}/avatar`
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const handleSelect = () => {
|
const handleSelect = () => {
|
||||||
setSelectedProfileId(isSelected ? null : profile.id);
|
setSelectedProfileId(isSelected ? null : profile.id);
|
||||||
@@ -81,7 +79,7 @@ export function ProfileCard({ profile }: ProfileCardProps) {
|
|||||||
alt={`${profile.name} avatar`}
|
alt={`${profile.name} avatar`}
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-full w-full object-cover transition-all duration-200',
|
'h-full w-full object-cover transition-all duration-200',
|
||||||
!isSelected && 'grayscale'
|
!isSelected && 'grayscale',
|
||||||
)}
|
)}
|
||||||
onError={() => setAvatarError(true)}
|
onError={() => setAvatarError(true)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ import { useSystemAudioCapture } from '@/lib/hooks/useSystemAudioCapture';
|
|||||||
import { useTranscription } from '@/lib/hooks/useTranscription';
|
import { useTranscription } from '@/lib/hooks/useTranscription';
|
||||||
import { isTauri } from '@/lib/tauri';
|
import { isTauri } from '@/lib/tauri';
|
||||||
import { formatAudioDuration, getAudioDuration } from '@/lib/utils/audio';
|
import { formatAudioDuration, getAudioDuration } from '@/lib/utils/audio';
|
||||||
import { type ProfileFormDraft, useUIStore } from '@/stores/uiStore';
|
|
||||||
import { useServerStore } from '@/stores/serverStore';
|
import { useServerStore } from '@/stores/serverStore';
|
||||||
|
import { type ProfileFormDraft, useUIStore } from '@/stores/uiStore';
|
||||||
import { AudioSampleRecording } from './AudioSampleRecording';
|
import { AudioSampleRecording } from './AudioSampleRecording';
|
||||||
import { AudioSampleSystem } from './AudioSampleSystem';
|
import { AudioSampleSystem } from './AudioSampleSystem';
|
||||||
import { AudioSampleUpload } from './AudioSampleUpload';
|
import { AudioSampleUpload } from './AudioSampleUpload';
|
||||||
@@ -427,7 +427,8 @@ export function ProfileForm() {
|
|||||||
} catch (avatarError) {
|
} catch (avatarError) {
|
||||||
toast({
|
toast({
|
||||||
title: 'Avatar upload failed',
|
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',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -520,7 +521,8 @@ export function ProfileForm() {
|
|||||||
} catch (avatarError) {
|
} catch (avatarError) {
|
||||||
toast({
|
toast({
|
||||||
title: 'Avatar upload failed',
|
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',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -436,21 +436,22 @@ class MLXSTTBackend:
|
|||||||
|
|
||||||
def _transcribe_sync():
|
def _transcribe_sync():
|
||||||
"""Run synchronous transcription in thread pool."""
|
"""Run synchronous transcription in thread pool."""
|
||||||
# Load audio
|
# MLX Whisper transcription using generate method
|
||||||
audio, sr = load_audio(audio_path, sample_rate=16000)
|
# The generate method accepts audio path directly
|
||||||
|
decode_options = {}
|
||||||
|
if language:
|
||||||
|
decode_options["language"] = language
|
||||||
|
|
||||||
# MLX Whisper transcription
|
result = self.model.generate(str(audio_path), **decode_options)
|
||||||
# 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)
|
# Extract text from result
|
||||||
if isinstance(result, str):
|
if isinstance(result, str):
|
||||||
return result.strip()
|
return result.strip()
|
||||||
elif isinstance(result, dict):
|
elif isinstance(result, dict):
|
||||||
return result.get("text", "").strip()
|
return result.get("text", "").strip()
|
||||||
|
elif hasattr(result, "text"):
|
||||||
|
return result.text.strip()
|
||||||
else:
|
else:
|
||||||
# Try to get text attribute
|
|
||||||
return str(result).strip()
|
return str(result).strip()
|
||||||
|
|
||||||
# Run blocking transcription in thread pool
|
# Run blocking transcription in thread pool
|
||||||
|
|||||||
+12
-10
@@ -5,7 +5,7 @@ import Image from 'next/image';
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { AppleIcon, LinuxIcon, WindowsIcon } from '@/components/PlatformIcons';
|
import { AppleIcon, LinuxIcon, WindowsIcon } from '@/components/PlatformIcons';
|
||||||
import { Button } from '@/components/ui/button';
|
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 { DOWNLOAD_LINKS, GITHUB_REPO } from '@/lib/constants';
|
||||||
import type { DownloadLinks } from '@/lib/releases';
|
import type { DownloadLinks } from '@/lib/releases';
|
||||||
import { FeatureCard } from '../components/ui/feature-card';
|
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.",
|
"Powered by Alibaba's Qwen3-TTS model for exceptional voice quality and accuracy.",
|
||||||
icon: <Zap className="h-6 w-6" />,
|
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',
|
title: 'Multi-Sample Support',
|
||||||
description:
|
description:
|
||||||
'Combine multiple voice samples for higher quality and more natural-sounding results.',
|
'Combine multiple voice samples for higher quality and more natural-sounding results.',
|
||||||
icon: <Code className="h-6 w-6" />,
|
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',
|
title: 'Local or Remote',
|
||||||
description:
|
description:
|
||||||
@@ -246,6 +248,10 @@ export default function Home() {
|
|||||||
model, clone any voice from a few seconds of audio, and compose multi-voice projects
|
model, clone any voice from a few seconds of audio, and compose multi-voice projects
|
||||||
with studio-grade editing tools.
|
with studio-grade editing tools.
|
||||||
</p>
|
</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>
|
<p className="text-foreground/60">No Python install required.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -281,10 +287,6 @@ export default function Home() {
|
|||||||
|
|
||||||
{/* Features Section */}
|
{/* Features Section */}
|
||||||
<Section id="features">
|
<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">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
|
||||||
{features.map((feature) => (
|
{features.map((feature) => (
|
||||||
<FeatureCard
|
<FeatureCard
|
||||||
|
|||||||
Generated
+1
-1
@@ -5041,7 +5041,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "voicebox"
|
name = "voicebox"
|
||||||
version = "0.1.10"
|
version = "0.1.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user