Compare commits

..
16 Commits
Author SHA1 Message Date
Jamie Pine 40fcb8d917 Bump version: 0.2.3 → 0.2.4 2026-03-15 10:18:51 -07:00
Jamie Pine ad64d1c3d9 Collect all zipvoice files for PyInstaller (fixes source code error) 2026-03-15 10:18:40 -07:00
Jamie Pine f826e45250 Install chatterbox-tts in CI release workflow 2026-03-15 10:17:23 -07:00
Jamie Pine 3d53c06c5b Bump version: 0.2.2 → 0.2.3 2026-03-15 10:08:56 -07:00
James Pine 9835b9f6d4 fix: prevent stale release data by removing Next.js fetch cache
Replace next: { revalidate: 600 } with cache: 'no-store' on GitHub
API fetches so new releases show up within 5 minutes (in-memory cache
only, no Next.js/Vercel cache layer on top).
2026-03-15 10:07:50 -07:00
Jamie Pine a15dd30b1e Update tauri-action to v0.6 to fix updater JSON and signature generation 2026-03-15 10:05:36 -07:00
Jamie Pine 1d343ac071 Treat missing/draft releases as up-to-date instead of showing error 2026-03-15 09:52:17 -07:00
James Pine ca602de0ae fix: don't reset audio player when unmuting during playback 2026-03-15 09:29:44 -07:00
James Pine cdc0293ca8 feat: add /linux-install page with build-from-source instructions
Linux download card now links to /linux-install instead of a direct
binary download. The page explains the CI situation and gives
clone + setup + build commands.
2026-03-15 09:17:30 -07:00
Jamie Pine e7f749f082 Add luxtts/zipvoice hidden imports to PyInstaller build 2026-03-15 09:13:59 -07:00
Jamie Pine d42e926e5c Bump version: 0.2.1 → 0.2.2 2026-03-15 09:02:10 -07:00
Jamie Pine 32768ea874 Add chatterbox hidden imports to PyInstaller build 2026-03-15 09:00:13 -07:00
James Pine b585e18ccf fix: fade in hero background glow to avoid Safari rendering flash 2026-03-15 08:53:08 -07:00
Jamie Pine 655910457f Auto-update CUDA binary on app update: check version on startup, download if stale 2026-03-15 08:46:17 -07:00
James Pine d6984f1057 fix: remove mix-blend-lighten and drop-shadow causing boxes in Safari 2026-03-15 08:45:40 -07:00
James Pine a637aebe69 feat: show version and total download count on landing page
Fetches download counts across all GitHub releases (paginated) and
displays version, total downloads, and platform list below the CTA.
2026-03-15 08:37:23 -07:00
21 changed files with 347 additions and 30 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.2.1
current_version = 0.2.4
commit = True
tag = True
tag_name = v{new_version}
+3 -1
View File
@@ -61,6 +61,7 @@ jobs:
python -m pip install --upgrade pip
pip install pyinstaller
pip install -r backend/requirements.txt
pip install --no-deps chatterbox-tts
- name: Install MLX dependencies (Apple Silicon only)
if: matrix.backend == 'mlx'
@@ -122,7 +123,7 @@ jobs:
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
- uses: tauri-apps/tauri-action@v0
- uses: tauri-apps/tauri-action@v0.6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
@@ -173,6 +174,7 @@ jobs:
python -m pip install --upgrade pip
pip install pyinstaller
pip install -r backend/requirements.txt
pip install --no-deps chatterbox-tts
- name: Install PyTorch with CUDA 12.1
run: |
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@voicebox/app",
"version": "0.2.1",
"version": "0.2.4",
"private": true,
"type": "module",
"scripts": {
@@ -246,13 +246,18 @@ export function GpuAcceleration() {
{/* CUDA download section - only show when no GPU is active (native or CUDA) */}
{!hasNativeGpu && !isCurrentlyCuda && (
<>
{/* Download progress */}
{/* Download progress (manual download or auto-update) */}
{cudaDownloading && downloadProgress && (
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
<span>{downloadProgress.filename || 'Downloading CUDA backend...'}</span>
<span>
{downloadProgress.filename ||
(cudaAvailable
? 'Updating CUDA backend...'
: 'Downloading CUDA backend...')}
</span>
</div>
{downloadProgress.total > 0 && (
<span className="text-muted-foreground">
+1 -1
View File
@@ -1,3 +1,3 @@
# Backend package
__version__ = "0.2.1"
__version__ = "0.2.4"
+9
View File
@@ -68,6 +68,15 @@ def build_server(cuda=False):
'--hidden-import', 'backend.utils.effects',
'--hidden-import', 'backend.versions',
'--hidden-import', 'pedalboard',
'--hidden-import', 'chatterbox',
'--hidden-import', 'chatterbox.tts_turbo',
'--hidden-import', 'chatterbox.mtl_tts',
'--hidden-import', 'backend.backends.chatterbox_backend',
'--hidden-import', 'backend.backends.chatterbox_turbo_backend',
'--hidden-import', 'backend.backends.luxtts_backend',
'--hidden-import', 'zipvoice',
'--hidden-import', 'zipvoice.luxvoice',
'--collect-all', 'zipvoice',
'--hidden-import', 'torch',
'--hidden-import', 'transformers',
'--hidden-import', 'fastapi',
+50
View File
@@ -199,6 +199,56 @@ async def download_cuda_binary(version: Optional[str] = None):
raise
def get_cuda_binary_version() -> Optional[str]:
"""Get the version of the installed CUDA binary, or None if not installed."""
import subprocess
cuda_path = get_cuda_binary_path()
if not cuda_path:
return None
try:
result = subprocess.run(
[str(cuda_path), "--version"],
capture_output=True, text=True, timeout=30,
)
# Output format: "voicebox-server 0.2.0"
for line in result.stdout.strip().splitlines():
if "voicebox-server" in line:
return line.split()[-1]
except Exception as e:
logger.warning(f"Could not get CUDA binary version: {e}")
return None
async def check_and_update_cuda_binary():
"""Check if the CUDA binary is outdated and auto-download if so.
Called on server startup. If a CUDA binary exists but its version
doesn't match the current app version, triggers a background download
of the updated CUDA binary. The download progress is visible to the
frontend via the existing SSE progress endpoint.
"""
cuda_path = get_cuda_binary_path()
if not cuda_path:
return # No CUDA binary installed, nothing to update
cuda_version = get_cuda_binary_version()
current_version = __version__
if cuda_version == current_version:
logger.info(f"CUDA binary is up to date (v{current_version})")
return
logger.info(
f"CUDA binary version mismatch: binary=v{cuda_version}, app=v{current_version}. "
f"Auto-downloading updated CUDA backend..."
)
try:
await download_cuda_binary()
except Exception as e:
logger.error(f"Auto-update of CUDA binary failed: {e}")
async def delete_cuda_binary() -> bool:
"""Delete the downloaded CUDA binary. Returns True if deleted."""
path = get_cuda_binary_path()
+4
View File
@@ -3104,6 +3104,10 @@ async def startup_event():
print(f"Backend: {backend_type.upper()}")
print(f"GPU available: {_get_gpu_status()}")
# Auto-update CUDA binary if installed but outdated
from .cuda_download import check_and_update_cuda_binary
_create_background_task(check_and_update_cuda_binary())
# Initialize progress manager with main event loop for thread-safe operations
try:
progress_manager = get_progress_manager()
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@voicebox/landing",
"version": "0.2.1",
"version": "0.2.4",
"description": "Landing page for voicebox.sh",
"scripts": {
"dev": "bun --bun next dev --turbo",
-1
View File
@@ -2,7 +2,6 @@ import { NextResponse } from 'next/server';
import { getLatestRelease } from '@/lib/releases';
export const dynamic = 'force-dynamic';
export const revalidate = 600; // Revalidate every 10 minutes
export async function GET() {
try {
+14
View File
@@ -115,6 +115,20 @@
animation: fadeUp 0.6s ease-out forwards;
}
.hero-glow-fade {
opacity: 0;
animation: fadeIn 2s ease-out 0.3s forwards;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* Noise texture overlay for hero glow */
/* .hero-glow::after {
content: "";
+169
View File
@@ -0,0 +1,169 @@
import type { Metadata } from 'next';
import { Footer } from '@/components/Footer';
import { Navbar } from '@/components/Navbar';
import { GITHUB_REPO } from '@/lib/constants';
export const metadata: Metadata = {
title: 'Linux Install - Voicebox',
description: 'Build Voicebox from source on Linux. Clone, setup, and build in three commands.',
};
export default function LinuxInstall() {
return (
<>
<Navbar />
<section className="relative pt-32 pb-24">
<div className="mx-auto max-w-2xl px-6">
<h1 className="text-3xl font-bold tracking-tight text-foreground">Install on Linux</h1>
<p className="mt-4 text-muted-foreground">
We&apos;re currently working through CI issues that prevent us from shipping a reliable
pre-built binary for Linux. In the meantime, building from source is straightforward and
takes just a few minutes.
</p>
<div className="mt-10 space-y-6">
{/* Prerequisites */}
<div>
<h2 className="text-sm font-medium text-muted-foreground uppercase tracking-wider mb-3">
Prerequisites
</h2>
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-1">
<li>
<a
href="https://git-scm.com"
target="_blank"
rel="noopener noreferrer"
className="text-foreground hover:underline"
>
Git
</a>
</li>
<li>
<a
href="https://www.rust-lang.org/tools/install"
target="_blank"
rel="noopener noreferrer"
className="text-foreground hover:underline"
>
Rust
</a>
</li>
<li>
<a
href="https://github.com/casey/just#installation"
target="_blank"
rel="noopener noreferrer"
className="text-foreground hover:underline"
>
just
</a>{' '}
— install via{' '}
<code className="text-xs bg-muted px-1.5 py-0.5 rounded">cargo install just</code>
</li>
<li>
<a
href="https://bun.sh"
target="_blank"
rel="noopener noreferrer"
className="text-foreground hover:underline"
>
Bun
</a>
</li>
<li>
Tauri system deps —{' '}
<a
href="https://v2.tauri.app/start/prerequisites/#linux"
target="_blank"
rel="noopener noreferrer"
className="text-foreground hover:underline"
>
see Tauri docs
</a>
</li>
</ul>
</div>
{/* Steps */}
<div>
<h2 className="text-sm font-medium text-muted-foreground uppercase tracking-wider mb-3">
Build from source
</h2>
<div className="space-y-3">
<div className="rounded-lg border border-border bg-card/60 p-4 font-mono text-sm">
<div className="text-muted-foreground select-none"># Clone the repo</div>
<div>git clone https://github.com/jamiepine/voicebox.git</div>
<div>cd voicebox</div>
</div>
<div className="rounded-lg border border-border bg-card/60 p-4 font-mono text-sm">
<div className="text-muted-foreground select-none">
# Install all dependencies (Python venv, JS deps, etc.)
</div>
<div>just setup</div>
</div>
<div className="rounded-lg border border-border bg-card/60 p-4 font-mono text-sm">
<div className="text-muted-foreground select-none"># Build the app</div>
<div>just build</div>
</div>
</div>
<p className="mt-4 text-sm text-muted-foreground">
The built app will be in{' '}
<code className="text-xs bg-muted px-1.5 py-0.5 rounded">
tauri/src-tauri/target/release/bundle/
</code>
</p>
</div>
{/* Dev mode */}
<div>
<h2 className="text-sm font-medium text-muted-foreground uppercase tracking-wider mb-3">
Or run in dev mode
</h2>
<div className="rounded-lg border border-border bg-card/60 p-4 font-mono text-sm">
<div className="text-muted-foreground select-none">
# Start the dev server with hot reload
</div>
<div>just dev</div>
</div>
</div>
</div>
{/* Links */}
<div className="mt-12 pt-8 border-t border-border flex flex-wrap gap-4 text-sm">
<a
href={GITHUB_REPO}
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground transition-colors"
>
GitHub Repo
</a>
<a
href={`${GITHUB_REPO}/issues`}
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Report an issue
</a>
<a
href={`${GITHUB_REPO}/blob/main/CONTRIBUTING.md`}
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Contributing guide
</a>
</div>
</div>
</section>
<Footer />
</>
);
}
+16 -13
View File
@@ -13,6 +13,8 @@ import type { DownloadLinks } from '@/lib/releases';
export default function Home() {
const [downloadLinks, setDownloadLinks] = useState<DownloadLinks>(DOWNLOAD_LINKS);
const [version, setVersion] = useState<string | null>(null);
const [totalDownloads, setTotalDownloads] = useState<number | null>(null);
useEffect(() => {
fetch('/api/releases')
@@ -22,6 +24,8 @@ export default function Home() {
})
.then((data) => {
if (data.downloadLinks) setDownloadLinks(data.downloadLinks);
if (data.version) setVersion(data.version);
if (data.totalDownloads != null) setTotalDownloads(data.totalDownloads);
})
.catch((error) => {
console.error('Failed to fetch release info:', error);
@@ -35,7 +39,7 @@ export default function Home() {
{/* ── Hero Section ─────────────────────────────────────────────── */}
<section className="relative pt-32 pb-16">
{/* Background glow */}
<div className="hero-glow pointer-events-none absolute inset-0 -top-32">
<div className="hero-glow hero-glow-fade pointer-events-none absolute inset-0 -top-32">
<div className="absolute left-1/2 top-0 -translate-x-1/2 w-[800px] h-[600px] rounded-full bg-accent/15 blur-[150px]" />
<div className="absolute left-1/2 top-12 -translate-x-1/2 w-[500px] h-[400px] rounded-full bg-accent/10 blur-[80px]" />
</div>
@@ -44,23 +48,19 @@ export default function Home() {
{/* Logo */}
<div
className="fade-in mx-auto mb-8 h-[120px] w-[120px] md:h-[160px] md:w-[160px]"
style={{
animationDelay: '0ms',
filter:
'drop-shadow(0 0 20px hsl(43 60% 50% / 0.4)) drop-shadow(0 0 60px hsl(43 60% 50% / 0.2))',
}}
style={{ animationDelay: '0ms' }}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="/voicebox-logo-app.webp"
alt="Voicebox"
className="h-full w-full object-contain mix-blend-lighten"
className="h-full w-full object-contain"
/>
</div>
{/* Headline */}
<div className="fade-in relative" style={{ animationDelay: '100ms' }}>
<h1 className="text-5xl font-bold tracking-tighter leading-[0.9] text-foreground drop-shadow-[0_16px_50px_rgba(0,0,0,0.95)] md:text-7xl lg:text-8xl">
<h1 className="text-5xl font-bold tracking-tighter leading-[0.9] text-foreground md:text-7xl lg:text-8xl">
Your voice, your machine.
</h1>
</div>
@@ -96,12 +96,16 @@ export default function Home() {
</a>
</div>
{/* Version */}
{/* Version + downloads */}
<p
className="fade-in mt-4 text-xs text-muted-foreground/50"
style={{ animationDelay: '400ms' }}
>
Free and open source &middot; macOS, Windows, Linux
{version ?? ''}
{version && totalDownloads != null ? ' \u00b7 ' : ''}
{totalDownloads != null ? `${totalDownloads.toLocaleString()} downloads` : ''}
{version || totalDownloads != null ? ' \u00b7 ' : ''}
macOS, Windows, Linux
</p>
</div>
@@ -290,14 +294,13 @@ export default function Home() {
{/* Linux */}
<a
href={downloadLinks.linux}
download
href="/linux-install"
className="flex items-center rounded-xl border border-border bg-card/60 backdrop-blur-sm px-5 py-4 transition-all hover:border-accent/30 hover:bg-card group"
>
<LinuxIcon className="h-6 w-6 shrink-0 text-muted-foreground group-hover:text-foreground transition-colors" />
<div className="ml-4">
<div className="text-sm font-medium">Linux</div>
<div className="text-xs text-muted-foreground">AppImage (x64)</div>
<div className="text-xs text-muted-foreground">Build from source</div>
</div>
</a>
</div>
@@ -20,7 +20,9 @@ export function unlockAudioContext() {
audioUnlocked = true;
// Unlock WaveSurfer's internal audio element
if (sharedWaveSurfer) {
// Skip if already playing — the context is already unlocked and the
// play/pause/reset dance would destroy the active playback.
if (sharedWaveSurfer && !sharedWaveSurfer.isPlaying()) {
const media = sharedWaveSurfer.getMediaElement();
if (media) {
media.muted = true;
+58 -2
View File
@@ -9,6 +9,7 @@ export interface DownloadLinks {
export interface ReleaseInfo {
version: string;
downloadLinks: DownloadLinks;
totalDownloads: number;
}
const GITHUB_REPO = 'jamiepine/voicebox';
@@ -17,7 +18,7 @@ const GITHUB_API_BASE = 'https://api.github.com';
// Cache for release info (in-memory cache, resets on server restart)
let cachedReleaseInfo: ReleaseInfo | null = null;
let cacheTimestamp: number = 0;
const CACHE_DURATION = 1000 * 60 * 10; // 10 minutes
const CACHE_DURATION = 1000 * 60 * 5; // 5 minutes
// Cache for star count
let cachedStarCount: number | null = null;
@@ -35,7 +36,7 @@ export async function getLatestRelease(): Promise<ReleaseInfo> {
try {
const response = await fetch(`${GITHUB_API_BASE}/repos/${GITHUB_REPO}/releases/latest`, {
next: { revalidate: 600 }, // Revalidate every 10 minutes
cache: 'no-store',
headers: {
Accept: 'application/vnd.github.v3+json',
},
@@ -72,11 +73,15 @@ export async function getLatestRelease(): Promise<ReleaseInfo> {
}
}
// Fetch total downloads across ALL releases
const totalDownloads = await getTotalDownloads();
// Fallback: construct URLs if not found in assets
const baseUrl = `https://github.com/${GITHUB_REPO}/releases/download/${version}`;
const releaseInfo: ReleaseInfo = {
version,
totalDownloads,
downloadLinks: {
macArm: downloadLinks.macArm || `${baseUrl}/voicebox_aarch64.app.tar.gz`,
macIntel: downloadLinks.macIntel || `${baseUrl}/voicebox_x64.app.tar.gz`,
@@ -97,6 +102,57 @@ export async function getLatestRelease(): Promise<ReleaseInfo> {
}
}
// Cache for total download count
let cachedTotalDownloads: number | null = null;
let downloadsCacheTimestamp: number = 0;
/**
* Fetches download counts across ALL releases (paginated)
*/
async function getTotalDownloads(): Promise<number> {
const now = Date.now();
if (cachedTotalDownloads !== null && now - downloadsCacheTimestamp < CACHE_DURATION) {
return cachedTotalDownloads;
}
let total = 0;
let page = 1;
try {
while (true) {
const response = await fetch(
`${GITHUB_API_BASE}/repos/${GITHUB_REPO}/releases?per_page=100&page=${page}`,
{
cache: 'no-store',
headers: { Accept: 'application/vnd.github.v3+json' },
},
);
if (!response.ok) break;
const releases = await response.json();
if (!Array.isArray(releases) || releases.length === 0) break;
for (const release of releases) {
for (const asset of release.assets || []) {
total += asset.download_count || 0;
}
}
if (releases.length < 100) break;
page++;
}
cachedTotalDownloads = total;
downloadsCacheTimestamp = now;
} catch (error) {
console.error('Failed to fetch total downloads:', error);
if (cachedTotalDownloads !== null) return cachedTotalDownloads;
}
return total;
}
/**
* Fetches the star count for the repo from GitHub
*/
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "voicebox",
"version": "0.2.1",
"version": "0.2.4",
"private": true,
"workspaces": [
"app",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@voicebox/tauri",
"private": true,
"version": "0.2.1",
"version": "0.2.4",
"type": "module",
"scripts": {
"dev": "vite",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "voicebox"
version = "0.2.1"
version = "0.2.4"
description = "A production-quality desktop app for Qwen3-TTS voice cloning and generation"
authors = ["you"]
license = ""
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Voicebox",
"version": "0.2.1",
"version": "0.2.4",
"identifier": "sh.voicebox.app",
"build": {
"beforeDevCommand": "bun run dev",
+5 -1
View File
@@ -64,13 +64,17 @@ class TauriUpdater implements PlatformUpdater {
}
this.notifySubscribers();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
// Tauri updater throws on 404 / no published release / network errors.
// Treat "no update available" style errors as up-to-date, not failures.
const isNoUpdate = /404|not found|no update|up.to.date/i.test(message);
this.status = {
checking: false,
available: false,
downloading: false,
installing: false,
readyToInstall: false,
error: error instanceof Error ? error.message : 'Failed to check for updates',
error: isNoUpdate ? undefined : message,
};
this.notifySubscribers();
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@voicebox/web",
"private": true,
"version": "0.2.1",
"version": "0.2.4",
"type": "module",
"scripts": {
"dev": "vite",