diff --git a/README.md b/README.md index 725460ab..0b67840b 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app/src/components/VoiceProfiles/ProfileCard.tsx b/app/src/components/VoiceProfiles/ProfileCard.tsx index ed46259b..e879294f 100644 --- a/app/src/components/VoiceProfiles/ProfileCard.tsx +++ b/app/src/components/VoiceProfiles/ProfileCard.tsx @@ -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)} /> diff --git a/app/src/components/VoiceProfiles/ProfileForm.tsx b/app/src/components/VoiceProfiles/ProfileForm.tsx index f7a84e15..90cc4fe0 100644 --- a/app/src/components/VoiceProfiles/ProfileForm.tsx +++ b/app/src/components/VoiceProfiles/ProfileForm.tsx @@ -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', }); } diff --git a/backend/backends/mlx_backend.py b/backend/backends/mlx_backend.py index 8eafc56e..7585c656 100644 --- a/backend/backends/mlx_backend.py +++ b/backend/backends/mlx_backend.py @@ -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) diff --git a/landing/src/app/page.tsx b/landing/src/app/page.tsx index be4869f3..46cf3556 100644 --- a/landing/src/app/page.tsx +++ b/landing/src/app/page.tsx @@ -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: , }, + { + title: 'Stories Editor', + description: + 'Create multi-voice narratives with a timeline-based editor. Arrange tracks, trim clips, and mix conversations.', + icon: , + }, { title: 'Multi-Sample Support', description: 'Combine multiple voice samples for higher quality and more natural-sounding results.', icon: , }, - { - title: 'Smart Caching', - description: 'Instant re-generation with voice prompt caching. No need to reprocess samples.', - icon: , - }, + { 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.

+

+ Optimized for performance with Metal acceleration on Mac and{' '} + CUDA acceleration on Windows/Linux for fast, local inference. +

No Python install required.

@@ -281,10 +287,6 @@ export default function Home() { {/* Features Section */}
- Features -

- Everything you need for professional voice cloning in a desktop app. -

{features.map((feature) => (