Files
voicebox/landing/src/lib/releases.ts
T
James Pine f80782a90a Landing page v0.2.0 updates: multi-engine copy, star count, model cards, voice creator section, responsive ControlUI, iOS audio fix
- Replace Qwen-specific copy with multi-engine messaging across hero, meta, and features
- Add GitHub star count fetched server-side via /api/stars with Spacedrive-style navbar badge
- Replace 'Why Voicebox exists' section with model cards for all 4 TTS engines
- Enable Linux download card (was 'Coming soon')
- Update GPU support copy to include ROCm, Intel Arc, DirectML
- Add Voice Creator section with animated 3-tab UI (upload, mic, system audio) and waveform background
- Make ControlUI responsive: horizontal scroll cards on mobile, stacked layout, scroll-to-active profile
- Fix iOS Safari audio autoplay (unlock AudioContext on user gesture)
- Fix hero logo square background with mix-blend-lighten
- Remove generation length green coloring, use gray with accent highlights
- Comment out grain overlay (visible tile seams)
- Remove player close button, stack waveform above controls on mobile
- Fixed-height profile cards (143px) with space between badges and buttons
2026-03-15 04:53:28 -07:00

134 lines
3.9 KiB
TypeScript

// Fetch latest release information from GitHub
export interface DownloadLinks {
macArm: string;
macIntel: string;
windows: string;
linux: string;
}
export interface ReleaseInfo {
version: string;
downloadLinks: DownloadLinks;
}
const GITHUB_REPO = 'jamiepine/voicebox';
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
// Cache for star count
let cachedStarCount: number | null = null;
let starCacheTimestamp: number = 0;
/**
* Fetches the latest release from GitHub and extracts download links
*/
export async function getLatestRelease(): Promise<ReleaseInfo> {
// Return cached data if still valid
const now = Date.now();
if (cachedReleaseInfo && now - cacheTimestamp < CACHE_DURATION) {
return cachedReleaseInfo;
}
try {
const response = await fetch(`${GITHUB_API_BASE}/repos/${GITHUB_REPO}/releases/latest`, {
next: { revalidate: 600 }, // Revalidate every 10 minutes
headers: {
Accept: 'application/vnd.github.v3+json',
},
});
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status}`);
}
const release = await response.json();
const version = release.tag_name;
const assets = release.assets || [];
// Extract download links based on file patterns
const downloadLinks: Partial<DownloadLinks> = {};
for (const asset of assets) {
const name = asset.name.toLowerCase();
const url = asset.browser_download_url;
// Skip signature files and other non-downloadable files
if (name.endsWith('.sig') || name.endsWith('.json') || name.endsWith('.txt')) {
continue;
}
if ((name.includes('aarch64') || name.includes('arm64')) && name.endsWith('.app.tar.gz')) {
downloadLinks.macArm = url;
} else if (name.includes('x64') && name.endsWith('.app.tar.gz')) {
downloadLinks.macIntel = url;
} else if (name.endsWith('.msi')) {
downloadLinks.windows = url;
} else if (name.endsWith('.appimage') || name.endsWith('.deb')) {
downloadLinks.linux = url;
}
}
// Fallback: construct URLs if not found in assets
const baseUrl = `https://github.com/${GITHUB_REPO}/releases/download/${version}`;
const releaseInfo: ReleaseInfo = {
version,
downloadLinks: {
macArm: downloadLinks.macArm || `${baseUrl}/voicebox_aarch64.app.tar.gz`,
macIntel: downloadLinks.macIntel || `${baseUrl}/voicebox_x64.app.tar.gz`,
windows:
downloadLinks.windows || `${baseUrl}/voicebox_${version.replace('v', '')}_x64_en-US.msi`,
linux: downloadLinks.linux || `${baseUrl}/voicebox_x86_64-unknown-linux-gnu.AppImage`,
},
};
// Update cache
cachedReleaseInfo = releaseInfo;
cacheTimestamp = now;
return releaseInfo;
} catch (error) {
console.error('Failed to fetch latest release:', error);
throw error;
}
}
/**
* Fetches the star count for the repo from GitHub
*/
export async function getStarCount(): Promise<number> {
const now = Date.now();
if (cachedStarCount !== null && now - starCacheTimestamp < CACHE_DURATION) {
return cachedStarCount;
}
try {
const response = await fetch(`${GITHUB_API_BASE}/repos/${GITHUB_REPO}`, {
next: { revalidate: 600 },
headers: {
Accept: 'application/vnd.github.v3+json',
},
});
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status}`);
}
const repo = await response.json();
const count = repo.stargazers_count ?? 0;
cachedStarCount = count;
starCacheTimestamp = now;
return count;
} catch (error) {
console.error('Failed to fetch star count:', error);
if (cachedStarCount !== null) return cachedStarCount;
throw error;
}
}