diff --git a/bun.lock b/bun.lock
index b507da48..d6b63ee8 100644
--- a/bun.lock
+++ b/bun.lock
@@ -17,7 +17,7 @@
},
"app": {
"name": "@voicebox/app",
- "version": "0.1.13",
+ "version": "0.2.0",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
@@ -72,13 +72,15 @@
},
"landing": {
"name": "@voicebox/landing",
- "version": "0.1.13",
+ "version": "0.2.0",
"dependencies": {
+ "@fontsource/space-grotesk": "^5.2.10",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"autoprefixer": "^10.4.17",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "framer-motion": "^12.36.0",
"lucide-react": "^0.316.0",
"next": "^16.1.3",
"postcss": "^8.4.33",
@@ -87,6 +89,7 @@
"tailwind-merge": "^3.4.0",
"tailwindcss": "^3.4.1",
"tailwindcss-animate": "^1.0.7",
+ "wavesurfer.js": "^7.12.2",
},
"devDependencies": {
"@types/node": "^20.11.5",
@@ -97,7 +100,7 @@
},
"tauri": {
"name": "@voicebox/tauri",
- "version": "0.1.13",
+ "version": "0.2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0",
"@tauri-apps/plugin-dialog": "^2.0.0",
@@ -120,7 +123,7 @@
},
"web": {
"name": "@voicebox/web",
- "version": "0.1.13",
+ "version": "0.2.0",
"dependencies": {
"@tanstack/react-query": "^5.0.0",
"react": "^18.3.0",
@@ -274,6 +277,8 @@
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
+ "@fontsource/space-grotesk": ["@fontsource/space-grotesk@5.2.10", "", {}, "sha512-XNXEbT74OIITPqw2H6HXwPDp85fy43uxfBwFR5PU+9sLnjuLj12KlhVM9nZVN6q6dlKjkuN8JisW/OBxwxgUew=="],
+
"@hookform/resolvers": ["@hookform/resolvers@3.10.0", "", { "peerDependencies": { "react-hook-form": "^7.0.0" } }, "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag=="],
"@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.13.0", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw=="],
@@ -1152,12 +1157,16 @@
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
+ "@voicebox/landing/framer-motion": ["framer-motion@12.36.0", "", { "dependencies": { "motion-dom": "^12.36.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-4PqYHAT7gev0ke0wos+PyrcFxI0HScjm3asgU8nSYa8YzJFuwgIvdj3/s3ZaxLq0bUSboIn19A2WS/MHwLCvfw=="],
+
"@voicebox/landing/lucide-react": ["lucide-react@0.316.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } }, "sha512-dTmYX1H4IXsRfVcj/KUxworV6814ApTl7iXaS21AimK2RUEl4j4AfOmqD3VR8phe5V91m4vEJ8tCK4uT1jE5nA=="],
"@voicebox/landing/tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
"@voicebox/landing/tailwindcss": ["tailwindcss@3.4.19", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ=="],
+ "@voicebox/landing/wavesurfer.js": ["wavesurfer.js@7.12.2", "", {}, "sha512-akVYISAHCw2gNw/7n8Pk/zH1Zz91WJyL/2MaNQCLD1XV3A226gKlWoDHWp9UdWqQ3zXnWttDf9ewZQQ3cxbOmQ=="],
+
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
@@ -1169,5 +1178,9 @@
"tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
+
+ "@voicebox/landing/framer-motion/motion-dom": ["motion-dom@12.36.0", "", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-Ep1pq8P88rGJ75om8lTCA13zqd7ywPGwCqwuWwin6BKc0hMLkVfcS6qKlRqEo2+t0DwoUcgGJfXwaiFn4AOcQA=="],
+
+ "@voicebox/landing/framer-motion/motion-utils": ["motion-utils@12.36.0", "", {}, "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg=="],
}
}
diff --git a/landing/package.json b/landing/package.json
index 60acdd13..88464f93 100644
--- a/landing/package.json
+++ b/landing/package.json
@@ -9,11 +9,13 @@
"lint": "next lint"
},
"dependencies": {
+ "@fontsource/space-grotesk": "^5.2.10",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"autoprefixer": "^10.4.17",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "framer-motion": "^12.36.0",
"lucide-react": "^0.316.0",
"next": "^16.1.3",
"postcss": "^8.4.33",
@@ -21,7 +23,8 @@
"react-dom": "^18.2.0",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^3.4.1",
- "tailwindcss-animate": "^1.0.7"
+ "tailwindcss-animate": "^1.0.7",
+ "wavesurfer.js": "^7.12.2"
},
"devDependencies": {
"@types/node": "^20.11.5",
diff --git a/landing/public/audio/fireship.webm b/landing/public/audio/fireship.webm
new file mode 100644
index 00000000..0561ea18
Binary files /dev/null and b/landing/public/audio/fireship.webm differ
diff --git a/landing/public/audio/jarvis.webm b/landing/public/audio/jarvis.webm
new file mode 100644
index 00000000..4eefdbe8
Binary files /dev/null and b/landing/public/audio/jarvis.webm differ
diff --git a/landing/public/audio/linus.webm b/landing/public/audio/linus.webm
new file mode 100644
index 00000000..63b7ad03
Binary files /dev/null and b/landing/public/audio/linus.webm differ
diff --git a/landing/public/audio/morganfreeman.webm b/landing/public/audio/morganfreeman.webm
new file mode 100644
index 00000000..eb5cddd4
Binary files /dev/null and b/landing/public/audio/morganfreeman.webm differ
diff --git a/landing/public/audio/samaltman.webm b/landing/public/audio/samaltman.webm
new file mode 100644
index 00000000..7400bece
Binary files /dev/null and b/landing/public/audio/samaltman.webm differ
diff --git a/landing/public/audio/samjackson.webm b/landing/public/audio/samjackson.webm
new file mode 100644
index 00000000..02f43a96
Binary files /dev/null and b/landing/public/audio/samjackson.webm differ
diff --git a/landing/public/voicebox-logo-app.webp b/landing/public/voicebox-logo-app.webp
new file mode 100644
index 00000000..9fedd439
Binary files /dev/null and b/landing/public/voicebox-logo-app.webp differ
diff --git a/landing/src/app/globals.css b/landing/src/app/globals.css
index da5b7b99..709d74b7 100644
--- a/landing/src/app/globals.css
+++ b/landing/src/app/globals.css
@@ -16,7 +16,7 @@
--secondary-foreground: 0 0% 0%;
--muted: 0 0% 96%;
--muted-foreground: 0 0% 45%;
- --accent: 0 0% 96%;
+ --accent: 43 50% 50%;
--accent-foreground: 0 0% 0%;
--destructive: 0 0% 0%;
--destructive-foreground: 0 0% 100%;
@@ -27,26 +27,52 @@
}
.dark {
- --background: 0 0% 3%;
- --foreground: 0 0% 98%;
- --card: 0 0% 8% / 0.6;
- --card-foreground: 0 0% 98%;
- --popover: 0 0% 8% / 0.8;
- --popover-foreground: 0 0% 98%;
- --primary: 0 0% 98%;
- --primary-foreground: 0 0% 8%;
- --secondary: 0 0% 12% / 0.5;
- --secondary-foreground: 0 0% 98%;
- --muted: 0 0% 12% / 0.4;
- --muted-foreground: 0 0% 65%;
- --accent: 0 0% 15% / 0.5;
- --accent-foreground: 0 0% 98%;
+ /* Surfaces -- slightly warm-tinted darks */
+ --background: 30 4% 4%;
+ --foreground: 30 10% 94%;
+ --card: 30 4% 7%;
+ --card-foreground: 30 10% 94%;
+ --popover: 30 4% 7%;
+ --popover-foreground: 30 10% 94%;
+ --primary: 30 10% 94%;
+ --primary-foreground: 30 4% 7%;
+ --secondary: 30 4% 10%;
+ --secondary-foreground: 30 10% 94%;
+ --muted: 30 3% 12%;
+ --muted-foreground: 30 5% 55%;
+ --accent: 43 50% 45%;
+ --accent-foreground: 30 10% 94%;
--destructive: 0 62% 50%;
- --destructive-foreground: 0 0% 98%;
- --border: 0 0% 15% / 0.5;
- --input: 0 0% 15% / 0.5;
- --ring: 0 0% 98% / 0.2;
- --radius: 1rem;
+ --destructive-foreground: 30 10% 94%;
+ --border: 30 4% 13%;
+ --input: 30 4% 13%;
+ --ring: 30 10% 94% / 0.2;
+ --radius: 0.75rem;
+
+ /* App-specific surface tokens */
+ --app: 30 4% 4%;
+ --app-box: 30 4% 7%;
+ --app-dark-box: 30 4% 5%;
+ --app-darker-box: 30 4% 3%;
+ --app-light-box: 30 4% 14%;
+ --app-line: 30 4% 13%;
+ --app-button: 30 4% 11%;
+ --app-hover: 30 4% 15%;
+ --app-selected: 30 4% 17%;
+
+ /* Text hierarchy */
+ --ink: 30 10% 94%;
+ --ink-dull: 30 5% 55%;
+ --ink-faint: 30 3% 38%;
+
+ /* Accent shades */
+ --accent-faint: 43 45% 55%;
+ --accent-deep: 43 55% 35%;
+ --accent-glow: 43 60% 50%;
+
+ /* Sidebar */
+ --sidebar: 30 4% 3%;
+ --sidebar-line: 30 4% 10%;
}
}
@@ -60,9 +86,9 @@
body {
@apply bg-background text-foreground antialiased;
overflow-x: hidden;
- background-image:
- radial-gradient(at 0% 0%, rgba(255, 255, 255, 0.03) 0px, transparent 50%),
- radial-gradient(at 100% 100%, rgba(255, 255, 255, 0.02) 0px, transparent 50%);
+ font-family:
+ ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
+ "Helvetica Neue", Arial, sans-serif;
}
}
@@ -71,3 +97,41 @@
text-wrap: balance;
}
}
+
+/* Staggered fade-in animation for hero elements */
+@keyframes fadeUp {
+ from {
+ opacity: 0;
+ transform: translateY(16px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.fade-in {
+ opacity: 0;
+ animation: fadeUp 0.6s ease-out forwards;
+}
+
+/* Noise texture overlay for hero glow */
+.hero-glow::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ z-index: 5;
+ pointer-events: none;
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.8' numOctaves='5' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
+ opacity: 0.3;
+ mix-blend-mode: overlay;
+}
+
+/* Scrollbar hiding */
+::-webkit-scrollbar {
+ display: none;
+}
+
+* {
+ scrollbar-width: none;
+}
diff --git a/landing/src/app/layout.tsx b/landing/src/app/layout.tsx
index f29ef15e..a5ba9383 100644
--- a/landing/src/app/layout.tsx
+++ b/landing/src/app/layout.tsx
@@ -1,17 +1,19 @@
import type { Metadata } from 'next';
-import { Inter } from 'next/font/google';
import './globals.css';
-import { Banner } from '@/components/Banner';
-import { Footer } from '@/components/Footer';
-import { Header } from '@/components/Header';
-
-const inter = Inter({ subsets: ['latin'], variable: '--font-sans' });
export const metadata: Metadata = {
- title: 'Voicebox - Open Source Voice Cloning Desktop App Powered by Qwen3-TTS',
+ title: 'Voicebox - Open Source Voice Cloning Desktop App',
description:
'Near-perfect voice cloning powered by Qwen3-TTS. Desktop app for Mac, Windows, and Linux. Multi-sample support, smart caching, local or remote inference.',
- keywords: ['voice cloning', 'TTS', 'Qwen3', 'desktop app', 'AI voice'],
+ keywords: [
+ 'voice cloning',
+ 'TTS',
+ 'Qwen3',
+ 'desktop app',
+ 'AI voice',
+ 'open source',
+ 'text to speech',
+ ],
icons: {
icon: [
{ url: '/favicon.png', type: 'image/png' },
@@ -20,8 +22,8 @@ export const metadata: Metadata = {
apple: [{ url: '/apple-touch-icon.png', sizes: '180x180', type: 'image/png' }],
},
openGraph: {
- title: 'voicebox',
- description: 'Professional voice cloning with Qwen3-TTS',
+ title: 'Voicebox',
+ description: 'Open source voice cloning. Local-first. Free forever.',
type: 'website',
url: 'https://voicebox.sh',
},
@@ -30,15 +32,8 @@ export const metadata: Metadata = {
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
-
-
-
-
-
- {children}
-
-
-
+
+ {children}
);
diff --git a/landing/src/app/page.tsx b/landing/src/app/page.tsx
index 4ba9d8c4..cc5728e3 100644
--- a/landing/src/app/page.tsx
+++ b/landing/src/app/page.tsx
@@ -1,304 +1,227 @@
'use client';
-import { Cloud, Code, Cpu, Github, Shield, Zap } from 'lucide-react';
+import { Github } from 'lucide-react';
import Image from 'next/image';
import { useEffect, useState } from 'react';
+import { ControlUI } from '@/components/ControlUI';
+import { Features } from '@/components/Features';
+import { Footer } from '@/components/Footer';
+import { Navbar } from '@/components/Navbar';
import { AppleIcon, LinuxIcon, WindowsIcon } from '@/components/PlatformIcons';
-import { Button } from '@/components/ui/button';
-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';
export default function Home() {
const [downloadLinks, setDownloadLinks] = useState(DOWNLOAD_LINKS);
useEffect(() => {
- // Fetch latest release info
fetch('/api/releases')
.then((res) => {
- if (!res.ok) {
- throw new Error('Failed to fetch releases');
- }
+ if (!res.ok) throw new Error('Failed to fetch releases');
return res.json();
})
.then((data) => {
- if (data.downloadLinks) {
- setDownloadLinks(data.downloadLinks);
- }
+ if (data.downloadLinks) setDownloadLinks(data.downloadLinks);
})
.catch((error) => {
console.error('Failed to fetch release info:', error);
- // Keep fallback links (releases page) on error
});
}, []);
- const features = [
- {
- title: 'Near-Perfect Voice Cloning',
- description:
- "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: 'Local or Remote',
- description:
- 'Run GPU inference locally or connect to a remote machine. One-click server setup.',
- icon: ,
- },
- {
- title: 'Audio Transcription',
- description:
- 'Powered by Whisper for accurate speech-to-text. Extract reference text from voice samples automatically.',
- icon: ,
- },
- {
- title: 'Cross-Platform',
- description: 'Available for macOS, Windows, and Linux. No Python installation required.',
- icon: ,
- },
- ];
return (
-
- {/* Hero Section */}
-
-
-
- {/* Left side - Content */}
-
-
-
-
-
- Voicebox
-
-
- Open source voice cloning powered by Qwen3-TTS. Create natural-sounding speech from
- text with near-perfect voice replication.
-
+ <>
+
- {/* Mobile: centered screenshot above download buttons */}
-
-
- {/* Download buttons under left content */}
-
-
-
- {/* Desktop: Large screenshot positioned off-screen */}
-
-
+ {/* ── Hero Section ─────────────────────────────────────────────── */}
+
+ {/* Background glow */}
+
-
- {/* Screenshots Section */}
-
-
- {/* Description Section */}
-
-
-
- What is Voicebox?
-
-
-
- Voicebox is a local-first voice cloning studio with DAW-like features
- for professional voice synthesis. Think of it as a{' '}
- local, free and open-source alternative to ElevenLabs — download
- models, clone voices, and generate speech entirely on your machine.
-
-
- Unlike cloud services that lock your voice data behind subscriptions, Voicebox gives
- you complete privacy, professional tools, and native performance. Download a voice
- 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.
-
-
-
-
- {/* Demo Video Section */}
-
-
-
- See it in action...
-
-
-
- {/** biome-ignore lint/a11y/useMediaCaption: not generating captions for this, ya damn linter */}
-
-
- Your browser does not support the video tag.
-
-
-
-
-
-
- {/* Features Section */}
-
-
- {features.map((feature) => (
-
+ {/* Logo */}
+
+
- ))}
+
+
+ {/* Headline */}
+
+
+ Your voice, your machine.
+
+
+
+ {/* Subtitle */}
+
+ Open source voice cloning studio powered by Qwen3-TTS. Clone any voice, generate natural
+ speech, and compose multi-voice projects — all running locally.
+
+
+ {/* CTAs */}
+
+
+ {/* Version */}
+
+ Free and open source · macOS, Windows, Linux
+
-
-
+
+ {/* ── ControlUI mockup ─────────────────────────────────────── */}
+
+
+
+
+
+ {/* ── Features ─────────────────────────────────────────────── */}
+
+
+ {/* ── About / Manifesto ────────────────────────────────────── */}
+
+
+
+ Why Voicebox exists
+
+
+
+ Cloud voice cloning services lock your voice data behind subscriptions, rate limits,
+ and terms of service that can change at any time. Your voice — and the voices you
+ clone — should belong to you.
+
+
+ Voicebox is a local-first voice cloning studio. Download a model, clone any voice from
+ a few seconds of audio, and generate speech entirely on your machine.
+
+
+ Optimized with Metal acceleration on Mac and CUDA on Windows/Linux. No Python install
+ required. No cloud. No subscriptions. Free and open-source, forever.
+
+
+
+
+
+ {/* ── Download Section ─────────────────────────────────────── */}
+
+
+
+
+ Download Voicebox
+
+
+ Available for macOS, Windows, and Linux. No dependencies required.
+
+
+
+
+
+ {/* GitHub link */}
+
+
+
+
+ {/* ── Footer ───────────────────────────────────────────────── */}
+
+ >
);
}
diff --git a/landing/src/components/ControlUI.tsx b/landing/src/components/ControlUI.tsx
new file mode 100644
index 00000000..886f1374
--- /dev/null
+++ b/landing/src/components/ControlUI.tsx
@@ -0,0 +1,733 @@
+'use client';
+
+import { motion } from 'framer-motion';
+import {
+ AudioLines,
+ Box,
+ Download,
+ Mic,
+ MoreHorizontal,
+ Pencil,
+ Server,
+ Sparkles,
+ Speaker,
+ Star,
+ Trash2,
+ Volume2,
+ Wand2,
+} from 'lucide-react';
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { LandingAudioPlayer } from './LandingAudioPlayer';
+
+// ─── Data ───────────────────────────────────────────────────────────────────
+// Edit this section to customise all the content shown in the ControlUI demo.
+
+interface VoiceProfile {
+ name: string;
+ description: string;
+ language: string;
+ hasEffects: boolean;
+}
+
+/** Voice profiles shown in the 3×3 grid. Index matters — DemoScript references profiles by index. */
+const PROFILES: VoiceProfile[] = [
+ {
+ name: 'Jarvis',
+ description: 'Dry wit, composed British AI assistant',
+ language: 'en',
+ hasEffects: true,
+ },
+ {
+ name: 'Samuel L. Jackson',
+ description: 'Commanding intensity with sharp, punchy delivery',
+ language: 'en',
+ hasEffects: true,
+ },
+ {
+ name: 'Bob Ross',
+ description: 'Gentle, soothing voice full of quiet encouragement',
+ language: 'en',
+ hasEffects: false,
+ },
+ {
+ name: 'Sam Altman',
+ description: 'Measured, thoughtful Silicon Valley cadence',
+ language: 'en',
+ hasEffects: false,
+ },
+ {
+ name: 'Morgan Freeman',
+ description: 'Rich, warm baritone with gravitas and calm authority',
+ language: 'en',
+ hasEffects: false,
+ },
+ {
+ name: 'Linus Tech Tips',
+ description: 'Enthusiastic, fast-paced tech explainer energy',
+ language: 'en',
+ hasEffects: false,
+ },
+ {
+ name: 'Fireship',
+ description: 'Rapid-fire, deadpan tech humor with zero filler',
+ language: 'en',
+ hasEffects: false,
+ },
+ {
+ name: 'Scarlett Johansson',
+ description: 'Smooth, low alto with understated warmth',
+ language: 'en',
+ hasEffects: false,
+ },
+ {
+ name: 'Dario Amodei',
+ description: 'Calm, precise articulation with academic depth',
+ language: 'en',
+ hasEffects: false,
+ },
+];
+
+/** Each entry is one cycle of the demo animation: select a profile → type text → generate → play audio. */
+interface DemoStep {
+ profileIndex: number;
+ text: string;
+ audioUrl: string;
+ engine: string;
+ duration: string;
+ effect?: string;
+}
+
+const DEMO_SCRIPT: DemoStep[] = [
+ {
+ profileIndex: 0,
+ text: 'Sir, I have completed the analysis. Your code has twelve critical vulnerabilities, your coffee is cold, and frankly your commit messages could use some work.',
+ audioUrl: '/audio/jarvis.webm',
+ engine: 'Qwen 1.7B',
+ duration: '0:10',
+ effect: 'Robot',
+ },
+ {
+ profileIndex: 4,
+ text: "I've narrated penguins, galaxies, and the entire history of mankind. But nothing prepared me for the moment a computer learned to do my job from a five second audio clip.",
+ audioUrl: '/audio/morganfreeman.webm',
+ engine: 'Qwen 1.7B',
+ duration: '0:11',
+ effect: 'Radio',
+ },
+ {
+ profileIndex: 3,
+ text: "Open source? [laugh] What's that?",
+ audioUrl: '/audio/samaltman.webm',
+ engine: 'Chatterbox',
+ duration: '0:03',
+ },
+ {
+ profileIndex: 1,
+ text: "So let me get this straight. You downloaded an app, pressed a button, and now there's two of me? The world was not ready for one",
+ audioUrl: '/audio/samjackson.webm',
+ engine: 'Qwen 1.7B',
+ duration: '0:10',
+ },
+ {
+ profileIndex: 5,
+ text: "So we got this voice cloning software and honestly it's kind of terrifying. Like, my wife could not tell the difference. Voicebox dot s h, link in the description!",
+ audioUrl: '/audio/linus.webm',
+ engine: 'Qwen 1.7B',
+ duration: '0:11',
+ },
+ {
+ profileIndex: 6,
+ text: 'This is Voicebox in one hundred seconds. It clones voices locally, it runs on your GPU, and no, OpenAI cannot hear you. Lets go.',
+ audioUrl: '/audio/fireship.webm',
+ engine: 'Qwen 0.6B',
+ duration: '0:09',
+ },
+];
+
+/** History rows pre-populated on first load. Oldest first visually (array index 0 = top row). */
+interface Generation {
+ id: number;
+ profileName: string;
+ text: string;
+ language: string;
+ engine: string;
+ duration: string;
+ timeAgo: string;
+ favorited: boolean;
+ versions: number;
+}
+
+const INITIAL_GENERATIONS: Generation[] = [
+ {
+ id: 1,
+ profileName: 'Morgan Freeman',
+ text: 'The neural pathways of human speech contain more complexity than any language model can fully capture, yet we keep pushing the boundaries of what is possible.',
+ language: 'en',
+ engine: 'Qwen 1.7B',
+ duration: '0:08',
+ timeAgo: '2 minutes ago',
+ favorited: true,
+ versions: 3,
+ },
+ {
+ id: 2,
+ profileName: 'Samuel L. Jackson',
+ text: 'In a world increasingly shaped by artificial intelligence, the human voice remains our most powerful tool for connection and storytelling.',
+ language: 'en',
+ engine: 'Qwen 1.7B',
+ duration: '0:07',
+ timeAgo: '15 minutes ago',
+ favorited: false,
+ versions: 1,
+ },
+ {
+ id: 3,
+ profileName: 'Jarvis',
+ text: 'The architecture of modern text-to-speech systems reveals an elegant interplay between transformer models and acoustic feature prediction.',
+ language: 'en',
+ engine: 'Qwen 0.6B',
+ duration: '0:09',
+ timeAgo: '1 hour ago',
+ favorited: false,
+ versions: 2,
+ },
+ {
+ id: 4,
+ profileName: 'Bob Ross',
+ text: 'Welcome to the next chapter. Every great story begins with a single voice, and today that voice can be yours.',
+ language: 'en',
+ engine: 'Chatterbox',
+ duration: '0:06',
+ timeAgo: '3 hours ago',
+ favorited: true,
+ versions: 1,
+ },
+ {
+ id: 5,
+ profileName: 'Linus Tech Tips',
+ text: 'Local inference gives you complete control over your voice data. No cloud, no subscriptions, no compromises.',
+ language: 'en',
+ engine: 'Qwen 1.7B',
+ duration: '0:05',
+ timeAgo: '5 hours ago',
+ favorited: false,
+ versions: 1,
+ },
+];
+
+const SIDEBAR_ITEMS = [
+ { icon: Volume2, label: 'Generate' },
+ { icon: AudioLines, label: 'Stories' },
+ { icon: Mic, label: 'Voices' },
+ { icon: Wand2, label: 'Effects' },
+ { icon: Speaker, label: 'Audio' },
+ { icon: Box, label: 'Models' },
+ { icon: Server, label: 'Server' },
+];
+
+// ─── Phase system ───────────────────────────────────────────────────────────
+
+type Phase = 'idle' | 'selecting' | 'typing' | 'generating' | 'complete' | 'playing';
+
+const PHASE_DURATIONS: Record
= {
+ idle: 2500,
+ selecting: 800,
+ typing: 6000,
+ generating: 2800,
+ complete: 1200,
+ playing: 4000,
+};
+
+// ─── Typewriter ─────────────────────────────────────────────────────────────
+
+function TypewriterText({ text, speed }: { text: string; speed?: number }) {
+ // Default: fill the typing phase duration, leaving 500ms buffer at the end
+ const resolvedSpeed =
+ speed ?? Math.max(20, Math.floor((PHASE_DURATIONS.typing - 500) / text.length));
+ const [displayed, setDisplayed] = useState('');
+ const indexRef = useRef(0);
+
+ useEffect(() => {
+ indexRef.current = 0;
+ setDisplayed('');
+ const interval = setInterval(() => {
+ indexRef.current += 1;
+ if (indexRef.current <= text.length) {
+ setDisplayed(text.slice(0, indexRef.current));
+ } else {
+ clearInterval(interval);
+ }
+ }, resolvedSpeed);
+ return () => clearInterval(interval);
+ }, [text, resolvedSpeed]);
+
+ return (
+ <>
+ {displayed}
+
+ >
+ );
+}
+
+// ─── Loading bars (simplified react-loaders replacement) ────────────────────
+
+function LoadingBars({ mode }: { mode: 'idle' | 'generating' | 'playing' }) {
+ const barColor = mode !== 'idle' ? 'bg-accent' : 'bg-muted-foreground/40';
+ return (
+
+ {[0, 1, 2, 3, 4].map((i) => (
+
+ ))}
+
+ );
+}
+
+// ─── Profile Card ───────────────────────────────────────────────────────────
+
+function ProfileCard({
+ profile,
+ selected,
+ selecting,
+}: {
+ profile: VoiceProfile;
+ selected: boolean;
+ selecting: boolean;
+}) {
+ return (
+
+ {profile.name}
+
+ {profile.description}
+
+
+
+ {profile.language}
+
+ {profile.hasEffects && }
+
+
+
+ );
+}
+
+// ─── History Row ────────────────────────────────────────────────────────────
+
+function HistoryRow({
+ gen,
+ mode,
+ isNew,
+}: {
+ gen: Generation;
+ mode: 'idle' | 'generating' | 'playing';
+ isNew: boolean;
+}) {
+ return (
+
+
+ {/* Status icon */}
+
+
+
+
+ {/* Meta info */}
+
+
{gen.profileName}
+
+ {gen.language}
+ {gen.engine}
+ {mode !== 'generating' && {gen.duration} }
+
+
+ {mode === 'generating' ? (
+ Generating...
+ ) : (
+ gen.timeAgo
+ )}
+
+
+
+ {/* Transcript */}
+
+
+ {/* Action buttons */}
+
+
+
+
+ {gen.versions > 1 && (
+
+
+
+ )}
+
+
+
+
+
+
+ );
+}
+
+// ─── Floating Generate Box ──────────────────────────────────────────────────
+
+function FloatingGenerateBox({
+ phase,
+ typingText,
+ selectedProfile,
+ effect,
+}: {
+ phase: Phase;
+ typingText: string;
+ selectedProfile: VoiceProfile | null;
+ effect?: string;
+}) {
+ const isFocused = phase === 'typing' || phase === 'generating';
+ const isGenerating = phase === 'generating';
+
+ return (
+
+
+ {/* Text area + generate button */}
+
+
+
+
+ {phase === 'typing' ? (
+
+
+
+ ) : phase === 'generating' ? (
+ {typingText}
+ ) : (
+
+ {selectedProfile
+ ? `Generate speech using ${selectedProfile.name}...`
+ : 'Select a voice profile above...'}
+
+ )}
+
+
+
+
+ {/* Generate button */}
+
+
+
+
+
+ {/* Bottom selectors */}
+
+
+ English
+
+
+ Qwen3-TTS 1.7B
+
+
+
+ {effect || 'Effect'}
+
+
+
+
+ );
+}
+
+// ─── Main ControlUI ─────────────────────────────────────────────────────────
+
+export function ControlUI() {
+ const [phase, setPhase] = useState('idle');
+ const [selectedIndex, setSelectedIndex] = useState(DEMO_SCRIPT[0].profileIndex);
+ const [cycle, setCycle] = useState(0);
+ const [newGenId, setNewGenId] = useState(null);
+ const [generations, setGenerations] = useState([...INITIAL_GENERATIONS]);
+ const [isMuted, setIsMuted] = useState(true);
+ const [isVisible, setIsVisible] = useState(true);
+ const [pageHidden, setPageHidden] = useState(false);
+ const containerRef = useRef(null);
+ const phaseRef = useRef(phase);
+ phaseRef.current = phase;
+
+ const step = DEMO_SCRIPT[cycle % DEMO_SCRIPT.length];
+ const selectedProfile = PROFILES[selectedIndex];
+
+ // Visibility detection
+ useEffect(() => {
+ const observer = new IntersectionObserver(([entry]) => setIsVisible(entry.isIntersecting), {
+ threshold: 0,
+ });
+ if (containerRef.current) observer.observe(containerRef.current);
+
+ const handleVisibility = () => setPageHidden(document.visibilityState !== 'visible');
+ document.addEventListener('visibilitychange', handleVisibility);
+
+ return () => {
+ observer.disconnect();
+ document.removeEventListener('visibilitychange', handleVisibility);
+ };
+ }, []);
+
+ const paused = !isVisible || pageHidden;
+
+ // Phase cycling — `playing` phase is driven by audio finish, not a timeout
+ useEffect(() => {
+ if (paused || phase === 'playing') return;
+
+ const duration = PHASE_DURATIONS[phase];
+ const timer = setTimeout(() => {
+ console.log(
+ '[ControlUI] phase transition',
+ phase,
+ '→ next, cycle:',
+ cycle,
+ 'step profile:',
+ PROFILES[step.profileIndex].name,
+ );
+ switch (phase) {
+ case 'idle': {
+ setSelectedIndex(step.profileIndex);
+ setPhase('selecting');
+ break;
+ }
+ case 'selecting':
+ setPhase('typing');
+ break;
+ case 'typing': {
+ const profile = PROFILES[step.profileIndex];
+ const newGen: Generation = {
+ id: Date.now(),
+ profileName: profile.name,
+ text: step.text,
+ language: profile.language,
+ engine: step.engine,
+ duration: step.duration,
+ timeAgo: 'just now',
+ favorited: false,
+ versions: 1,
+ };
+ setGenerations((prev) => [newGen, ...prev.slice(0, 5)]);
+ setNewGenId(newGen.id);
+ setPhase('generating');
+ break;
+ }
+ case 'generating':
+ setPhase('playing');
+ break;
+ }
+ }, duration);
+
+ return () => clearTimeout(timer);
+ }, [phase, paused, step, cycle]);
+
+ const handleAudioFinish = useCallback(() => {
+ if (phaseRef.current !== 'playing') return;
+ setPhase('idle');
+ setCycle((c) => c + 1);
+ setNewGenId(null);
+ }, []);
+
+ const isGenerating = phase === 'generating';
+
+ return (
+
+ {/* Unmute button */}
+
+ setIsMuted(!isMuted)}
+ className="flex items-center gap-2 px-3 py-1.5 rounded-full border border-border bg-card/50 backdrop-blur text-xs text-muted-foreground hover:text-foreground transition-colors"
+ >
+ {isMuted ? (
+ <>
+
+ Unmute
+ >
+ ) : (
+ <>
+
+ Mute
+ >
+ )}
+
+
+
+
+
+ {/* ── Sidebar ───────────────────────────────────────────── */}
+
+ {/* Logo */}
+
+
+
+
+
+
+ {/* Nav items */}
+
+ {SIDEBAR_ITEMS.map((item, i) => {
+ const Icon = item.icon;
+ const active = i === 0;
+ return (
+
+
+
+ );
+ })}
+
+
+ {/* Version */}
+
v0.2.0
+
+
+ {/* ── Main content ──────────────────────────────────────── */}
+
+ {/* Left: Profiles */}
+
+ {/* Header */}
+
+
Voicebox
+
+
+ Import Voice
+
+
+
+ Create Voice
+
+
+
+
+ {/* Profile grid */}
+
+
+ {PROFILES.map((profile, i) => (
+
+ ))}
+
+
+
+ {/* Floating generate box */}
+
+
+
+ {/* Right: History */}
+
+
+
+ {generations.map((gen) => {
+ const isThisNew = gen.id === newGenId;
+ const rowMode: 'idle' | 'generating' | 'playing' =
+ isThisNew && isGenerating
+ ? 'generating'
+ : isThisNew && phase === 'playing'
+ ? 'playing'
+ : 'idle';
+ return ;
+ })}
+
+
+
+
+ {/* Audio player */}
+
{}}
+ />
+
+
+
+
+ );
+}
diff --git a/landing/src/components/Features.tsx b/landing/src/components/Features.tsx
new file mode 100644
index 00000000..498c742d
--- /dev/null
+++ b/landing/src/components/Features.tsx
@@ -0,0 +1,476 @@
+'use client';
+
+import { motion } from 'framer-motion';
+import { AudioLines, Cloud, Layers, MessageSquareText, Mic, Monitor } from 'lucide-react';
+import { useEffect, useRef, useState } from 'react';
+
+// ─── Lazy load wrapper ──────────────────────────────────────────────────────
+
+function LazyLoad({
+ children,
+ className,
+ rootMargin = '200px',
+}: {
+ children: React.ReactNode;
+ className?: string;
+ rootMargin?: string;
+}) {
+ const ref = useRef(null);
+ const [visible, setVisible] = useState(false);
+
+ useEffect(() => {
+ const el = ref.current;
+ if (!el) return;
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ if (entry.isIntersecting) {
+ setVisible(true);
+ observer.disconnect();
+ }
+ },
+ { rootMargin },
+ );
+ observer.observe(el);
+ return () => observer.disconnect();
+ }, [rootMargin]);
+
+ return (
+
+ {visible ? children : null}
+
+ );
+}
+
+// ─── Animation: Voice Cloning ───────────────────────────────────────────────
+
+function VoiceCloningAnimation() {
+ const [phase, setPhase] = useState(0);
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setPhase((p) => (p + 1) % 3);
+ }, 2400);
+ return () => clearInterval(interval);
+ }, []);
+
+ const samples = ['Sample 1', 'Sample 2', 'Sample 3'];
+ const bars = [0.4, 0.7, 0.5, 0.9, 0.3, 0.6, 0.8, 0.4, 0.7, 0.5, 0.3, 0.6];
+
+ return (
+
+
+ {/* Sample pills */}
+
+ {samples.map((s, i) => (
+
+ {s}
+
+ ))}
+
+
+ {/* Waveform visualization */}
+
+ {bars.map((h, i) => (
+
+ ))}
+
+
+ {/* Result label */}
+
+ voice profile ready
+
+
+
+ );
+}
+
+// ─── Animation: Stories Editor ───────────────────────────────────────────────
+
+function StoriesAnimation() {
+ const [activeTrack, setActiveTrack] = useState(0);
+ const tracks = [
+ {
+ name: 'Narrator',
+ color: '#ac8a2b',
+ clips: [
+ { w: '45%', x: '5%' },
+ { w: '30%', x: '60%' },
+ ],
+ },
+ {
+ name: 'Character A',
+ color: '#3b82f6',
+ clips: [
+ { w: '25%', x: '15%' },
+ { w: '35%', x: '50%' },
+ ],
+ },
+ { name: 'Character B', color: '#8b5cf6', clips: [{ w: '20%', x: '30%' }] },
+ ];
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setActiveTrack((p) => (p + 1) % tracks.length);
+ }, 2200);
+ return () => clearInterval(interval);
+ }, [tracks.length]);
+
+ return (
+
+ {tracks.map((track, i) => (
+
+
+ {track.name}
+
+
+ {track.clips.map((clip, j) => (
+
+ ))}
+
+
+ ))}
+
+ );
+}
+
+// ─── Animation: Multi-Sample ────────────────────────────────────────────────
+
+function MultiSampleAnimation() {
+ const [active, setActive] = useState(0);
+ const samples = [
+ { label: 'interview_clip.wav', dur: '12.4s', quality: 0.92 },
+ { label: 'podcast_intro.wav', dur: '8.1s', quality: 0.88 },
+ { label: 'narration_take3.wav', dur: '15.7s', quality: 0.95 },
+ { label: 'casual_chat.wav', dur: '6.2s', quality: 0.85 },
+ ];
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setActive((p) => (p + 1) % samples.length);
+ }, 2000);
+ return () => clearInterval(interval);
+ }, [samples.length]);
+
+ return (
+
+ {samples.map((s, i) => (
+
+
+ {s.label}
+
+ {s.dur}
+
+
+
+
+ ))}
+
+ );
+}
+
+// ─── Animation: Local or Remote ─────────────────────────────────────────────
+
+function LocalRemoteAnimation() {
+ const [mode, setMode] = useState(0);
+ const modes = ['Local GPU', 'Remote Server'];
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setMode((p) => (p + 1) % 2);
+ }, 2800);
+ return () => clearInterval(interval);
+ }, []);
+
+ return (
+
+
+ {/* Toggle */}
+
+ {modes.map((m, i) => (
+
+ {m}
+
+ ))}
+
+
+ {/* Status */}
+
+
+
+ {mode === 0 ? 'Metal acceleration active' : 'Connected to 192.168.1.50'}
+
+
+ {mode === 0 ? 'VRAM: 8.2 / 16.0 GB' : 'Latency: 12ms | CUDA'}
+
+
+
+
+ );
+}
+
+// ─── Animation: Transcription ───────────────────────────────────────────────
+
+function TranscriptionAnimation() {
+ const [charIndex, setCharIndex] = useState(0);
+ const text = 'The quick brown fox jumps over the lazy dog near the riverbank.';
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setCharIndex((p) => {
+ if (p >= text.length) return 0;
+ return p + 1;
+ });
+ }, 80);
+ return () => clearInterval(interval);
+ }, [text.length]);
+
+ return (
+
+ {/* Fake waveform */}
+
+ {Array.from({ length: 30 }, (_, i) => {
+ const h = 0.2 + 0.8 * Math.abs(Math.sin(i * 0.5 + charIndex * 0.1));
+ const active = i < (charIndex / text.length) * 30;
+ return (
+
+ );
+ })}
+
+
+ {/* Transcribed text */}
+
+ {text.slice(0, charIndex)}
+ {charIndex < text.length && (
+
+ )}
+
+
+ );
+}
+
+// ─── Animation: Cross-Platform ──────────────────────────────────────────────
+
+function CrossPlatformAnimation() {
+ const [active, setActive] = useState(0);
+ const platforms = [
+ { name: 'macOS', icon: '🍎', detail: 'Metal acceleration' },
+ { name: 'Windows', icon: '🪟', detail: 'CUDA acceleration' },
+ { name: 'Linux', icon: '🐧', detail: 'CUDA acceleration' },
+ ];
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setActive((p) => (p + 1) % platforms.length);
+ }, 2000);
+ return () => clearInterval(interval);
+ }, [platforms.length]);
+
+ return (
+
+
+ {platforms.map((p, i) => (
+
+ {p.icon}
+
+ {p.name}
+
+
+ {p.detail}
+
+
+ ))}
+
+
+ );
+}
+
+// ─── Feature data ───────────────────────────────────────────────────────────
+
+const FEATURES = [
+ {
+ title: 'Near-Perfect Voice Cloning',
+ description:
+ 'Powered by Qwen3-TTS for exceptional voice quality. Clone any voice from a few seconds of audio with natural intonation and emotion.',
+ icon: Mic,
+ animation: VoiceCloningAnimation,
+ },
+ {
+ title: 'Stories Editor',
+ description:
+ 'Create multi-voice narratives with a timeline-based editor. Arrange tracks, trim clips, and mix conversations between characters.',
+ icon: AudioLines,
+ animation: StoriesAnimation,
+ },
+ {
+ title: 'Multi-Sample Support',
+ description:
+ 'Combine multiple voice samples for higher quality results. More samples means more natural-sounding speech synthesis.',
+ icon: Layers,
+ animation: MultiSampleAnimation,
+ },
+ {
+ title: 'Local or Remote',
+ description:
+ 'Run GPU inference locally with Metal or CUDA, or connect to a remote machine. One-click server setup with automatic discovery.',
+ icon: Cloud,
+ animation: LocalRemoteAnimation,
+ },
+ {
+ title: 'Audio Transcription',
+ description:
+ 'Powered by Whisper for accurate speech-to-text. Automatically extract reference text from voice samples.',
+ icon: MessageSquareText,
+ animation: TranscriptionAnimation,
+ },
+ {
+ title: 'Cross-Platform',
+ description:
+ 'Available for macOS, Windows, and Linux. No Python installation required — everything is bundled.',
+ icon: Monitor,
+ animation: CrossPlatformAnimation,
+ },
+];
+
+// ─── Feature Card ───────────────────────────────────────────────────────────
+
+function FeatureCard({ feature }: { feature: (typeof FEATURES)[number] }) {
+ const Icon = feature.icon;
+ const Animation = feature.animation;
+
+ return (
+
+
+
+
+
+
+
+
{feature.title}
+
+
{feature.description}
+
+
+ );
+}
+
+// ─── Features Section ───────────────────────────────────────────────────────
+
+export function Features() {
+ return (
+
+
+
+
+ Professional voice tools, zero compromise
+
+
+ Everything you need to clone voices, generate speech, and produce multi-voice content —
+ running entirely on your machine.
+
+
+
+ {FEATURES.map((feature) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/landing/src/components/Footer.tsx b/landing/src/components/Footer.tsx
index 9370ac86..9e435a82 100644
--- a/landing/src/components/Footer.tsx
+++ b/landing/src/components/Footer.tsx
@@ -1,22 +1,33 @@
+import Image from 'next/image';
import Link from 'next/link';
-import { Separator } from '@/components/ui/separator';
import { GITHUB_REPO } from '@/lib/constants';
export function Footer() {
return (
-
-
-
-
-
voicebox
-
- Professional voice cloning powered by Qwen3-TTS. Desktop app for Mac, Windows, and
- Linux.
+
+
+
+ {/* Brand */}
+
+
+
+ Voicebox
+
+
+ Open source voice cloning studio. Local-first, free forever.
+
+ {/* Product */}
+
+ {/* Resources */}
+
+ {/* Also by */}
+
-
-
-
© 2026 voicebox. All rights reserved.
+
+
+
+ © {new Date().getFullYear()} Voicebox. Open source under MIT license.
+
diff --git a/landing/src/components/LandingAudioPlayer.tsx b/landing/src/components/LandingAudioPlayer.tsx
new file mode 100644
index 00000000..a3b105ce
--- /dev/null
+++ b/landing/src/components/LandingAudioPlayer.tsx
@@ -0,0 +1,266 @@
+'use client';
+
+import { Pause, Play, Repeat, Volume2, VolumeX, X } from 'lucide-react';
+import { useCallback, useEffect, useRef, useState } from 'react';
+import WaveSurfer from 'wavesurfer.js';
+
+function formatDuration(seconds: number): string {
+ const m = Math.floor(seconds / 60);
+ const s = Math.floor(seconds % 60);
+ return `${m}:${s.toString().padStart(2, '0')}`;
+}
+
+interface LandingAudioPlayerProps {
+ audioUrl: string;
+ title: string;
+ playing: boolean;
+ muted: boolean;
+ onFinish: () => void;
+ onClose: () => void;
+}
+
+export function LandingAudioPlayer({
+ audioUrl,
+ title,
+ playing,
+ muted,
+ onFinish,
+ onClose,
+}: LandingAudioPlayerProps) {
+ const waveformRef = useRef
(null);
+ const wavesurferRef = useRef(null);
+ const [isPlaying, setIsPlaying] = useState(false);
+ const [currentTime, setCurrentTime] = useState(0);
+ const [duration, setDuration] = useState(0);
+ const [volume, setVolume] = useState(0.75);
+ const [isLooping, setIsLooping] = useState(false);
+ const [isReady, setIsReady] = useState(false);
+ const onFinishRef = useRef(onFinish);
+ onFinishRef.current = onFinish;
+ const playingRef = useRef(playing);
+ playingRef.current = playing;
+ const mutedRef = useRef(muted);
+ mutedRef.current = muted;
+
+ // Initialize WaveSurfer
+ useEffect(() => {
+ const initWaveSurfer = () => {
+ const container = waveformRef.current;
+ if (!container) {
+ setTimeout(initWaveSurfer, 50);
+ return;
+ }
+
+ const rect = container.getBoundingClientRect();
+ if (rect.width === 0 || rect.height === 0) {
+ setTimeout(initWaveSurfer, 50);
+ return;
+ }
+
+ // Clean up existing instance
+ if (wavesurferRef.current) {
+ wavesurferRef.current.destroy();
+ wavesurferRef.current = null;
+ }
+
+ const root = document.documentElement;
+ const getCSSVar = (varName: string) => {
+ const value = getComputedStyle(root).getPropertyValue(varName).trim();
+ return value ? `hsl(${value})` : '';
+ };
+
+ const ws = WaveSurfer.create({
+ container,
+ waveColor: getCSSVar('--muted'),
+ progressColor: getCSSVar('--accent'),
+ cursorColor: getCSSVar('--accent'),
+ barWidth: 2,
+ barRadius: 2,
+ height: 80,
+ normalize: true,
+ interact: true,
+ mediaControls: false,
+ });
+
+ ws.on('ready', () => {
+ setDuration(ws.getDuration());
+ ws.setVolume(mutedRef.current ? 0 : volume);
+ setIsReady(true);
+ });
+
+ ws.on('play', () => {
+ console.log('[Player] play event');
+ setIsPlaying(true);
+ });
+ ws.on('pause', () => {
+ console.log('[Player] pause event');
+ setIsPlaying(false);
+ });
+
+ ws.on('timeupdate', (time: number) => {
+ setCurrentTime(Math.min(time, ws.getDuration()));
+ });
+
+ let didFinish = false;
+ ws.on('finish', () => {
+ if (didFinish) return;
+ didFinish = true;
+ console.log(
+ '[Player] finish event, currentTime:',
+ ws.getCurrentTime(),
+ 'duration:',
+ ws.getDuration(),
+ );
+ setIsPlaying(false);
+ onFinishRef.current();
+ });
+
+ ws.load(audioUrl);
+ wavesurferRef.current = ws;
+ };
+
+ setIsReady(false);
+ setCurrentTime(0);
+ setDuration(0);
+
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ setTimeout(initWaveSurfer, 10);
+ });
+ });
+
+ return () => {
+ if (wavesurferRef.current) {
+ wavesurferRef.current.destroy();
+ wavesurferRef.current = null;
+ }
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [audioUrl]);
+
+ // Respond to external play/stop signals
+ useEffect(() => {
+ const ws = wavesurferRef.current;
+ console.log('[Player] effect', { playing, isReady, hasWs: !!ws });
+ if (!ws || !isReady) return;
+
+ if (playing) {
+ ws.play()
+ .then(() => {
+ console.log(
+ '[Player] play succeeded, isPlaying:',
+ ws.isPlaying(),
+ 'currentTime:',
+ ws.getCurrentTime(),
+ );
+ setTimeout(() => {
+ console.log(
+ '[Player] 1s later, isPlaying:',
+ ws.isPlaying(),
+ 'currentTime:',
+ ws.getCurrentTime(),
+ );
+ }, 1000);
+ })
+ .catch((e) => console.error('[Player] play failed', e));
+ } else {
+ ws.pause();
+ }
+ }, [playing, isReady]);
+
+ // Sync volume and muted state
+ useEffect(() => {
+ if (wavesurferRef.current) {
+ wavesurferRef.current.setVolume(muted ? 0 : volume);
+ }
+ }, [volume, muted]);
+
+ const handlePlayPause = useCallback(() => {
+ if (!wavesurferRef.current) return;
+ wavesurferRef.current.playPause();
+ }, []);
+
+ const handleClose = useCallback(() => {
+ if (wavesurferRef.current) {
+ wavesurferRef.current.pause();
+ }
+ setIsPlaying(false);
+ onClose();
+ }, [onClose]);
+
+ return (
+
+
+
+ {/* Play/Pause */}
+
+ {isPlaying ? : }
+
+
+ {/* Waveform */}
+
+
+ {/* Time */}
+
+ {formatDuration(currentTime)}
+ /
+ {formatDuration(duration)}
+
+
+ {/* Title */}
+ {title && (
+
+ {title}
+
+ )}
+
+ {/* Loop */}
+
setIsLooping(!isLooping)}
+ className={`h-8 w-8 flex items-center justify-center rounded-sm shrink-0 hover:bg-muted ${
+ isLooping ? 'text-foreground' : 'text-muted-foreground'
+ }`}
+ >
+
+
+
+ {/* Volume */}
+
+ setVolume(volume > 0 ? 0 : 0.75)}
+ className="h-8 w-8 flex items-center justify-center hover:bg-muted rounded-sm"
+ >
+ {volume > 0 ? (
+
+ ) : (
+
+ )}
+
+ setVolume(Number(e.target.value) / 100)}
+ className="flex-1 h-1 appearance-none bg-muted rounded-full accent-foreground cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-foreground"
+ />
+
+
+ {/* Close */}
+
+
+
+
+
+
+ );
+}
diff --git a/landing/src/components/Navbar.tsx b/landing/src/components/Navbar.tsx
new file mode 100644
index 00000000..41eb6c76
--- /dev/null
+++ b/landing/src/components/Navbar.tsx
@@ -0,0 +1,59 @@
+'use client';
+
+import { Github } from 'lucide-react';
+import Image from 'next/image';
+import { GITHUB_REPO } from '@/lib/constants';
+
+export function Navbar() {
+ return (
+
+
+
+ );
+}
diff --git a/landing/tailwind.config.js b/landing/tailwind.config.js
index 0a06b883..274291d1 100644
--- a/landing/tailwind.config.js
+++ b/landing/tailwind.config.js
@@ -8,6 +8,9 @@ module.exports = {
],
theme: {
extend: {
+ fontFamily: {
+ sans: ['var(--font-sans)', 'system-ui', 'sans-serif'],
+ },
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
@@ -33,6 +36,9 @@ module.exports = {
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
+ faint: 'hsl(var(--accent-faint))',
+ deep: 'hsl(var(--accent-deep))',
+ glow: 'hsl(var(--accent-glow))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
@@ -42,6 +48,27 @@ module.exports = {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
+ // App surface tokens
+ app: {
+ DEFAULT: 'hsl(var(--app))',
+ box: 'hsl(var(--app-box))',
+ darkBox: 'hsl(var(--app-dark-box))',
+ darkerBox: 'hsl(var(--app-darker-box))',
+ lightBox: 'hsl(var(--app-light-box))',
+ line: 'hsl(var(--app-line))',
+ button: 'hsl(var(--app-button))',
+ hover: 'hsl(var(--app-hover))',
+ selected: 'hsl(var(--app-selected))',
+ },
+ ink: {
+ DEFAULT: 'hsl(var(--ink))',
+ dull: 'hsl(var(--ink-dull))',
+ faint: 'hsl(var(--ink-faint))',
+ },
+ sidebar: {
+ DEFAULT: 'hsl(var(--sidebar))',
+ line: 'hsl(var(--sidebar-line))',
+ },
},
borderRadius: {
lg: 'var(--radius)',