diff --git a/app/package.json b/app/package.json
index e36d449c..4198caeb 100644
--- a/app/package.json
+++ b/app/package.json
@@ -40,6 +40,7 @@
"date-fns": "^3.6.0",
"framer-motion": "^12.29.0",
"lucide-react": "^0.454.0",
+ "motion": "^12.29.0",
"react": "^18.3.0",
"react-dom": "^18.3.0",
"react-hook-form": "^7.53.0",
diff --git a/app/src/App.tsx b/app/src/App.tsx
index 5b4fbce6..96f75043 100644
--- a/app/src/App.tsx
+++ b/app/src/App.tsx
@@ -1,24 +1,49 @@
import { useState, useEffect } from 'react';
+import voiceboxLogo from '@/assets/voicebox-logo.png';
+import { AudioPlayer } from '@/components/AudioPlayer/AudioPlayer';
import { GenerationForm } from '@/components/Generation/GenerationForm';
import { HistoryTable } from '@/components/History/HistoryTable';
import { ConnectionForm } from '@/components/ServerSettings/ConnectionForm';
+import { ModelManagement } from '@/components/ServerSettings/ModelManagement';
import { ServerStatus } from '@/components/ServerSettings/ServerStatus';
import { UpdateStatus } from '@/components/ServerSettings/UpdateStatus';
-import { ModelManagement } from '@/components/ServerSettings/ModelManagement';
+import ShinyText from '@/components/ShinyText';
+import { Sidebar } from '@/components/Sidebar';
+import { UpdateNotification } from '@/components/UpdateNotification';
import { Toaster } from '@/components/ui/toaster';
import { ProfileList } from '@/components/VoiceProfiles/ProfileList';
-import { Sidebar } from '@/components/Sidebar';
-import { AudioPlayer } from '@/components/AudioPlayer/AudioPlayer';
-import { UpdateNotification } from '@/components/UpdateNotification';
-import { isTauri, startServer, setupWindowCloseHandler } from '@/lib/tauri';
-import voiceboxLogo from '@/assets/voicebox-logo.png';
+import { isTauri, setupWindowCloseHandler, startServer } from '@/lib/tauri';
// Track if server is starting to prevent duplicate starts
let serverStarting = false;
+const LOADING_MESSAGES = [
+ 'Warming up tensors...',
+ 'Calibrating synthesizer engine...',
+ 'Initializing voice models...',
+ 'Loading neural networks...',
+ 'Preparing audio pipelines...',
+ 'Optimizing waveform generators...',
+ 'Tuning frequency analyzers...',
+ 'Building voice embeddings...',
+ 'Configuring text-to-speech cores...',
+ 'Syncing audio buffers...',
+ 'Establishing model connections...',
+ 'Preprocessing training data...',
+ 'Validating voice samples...',
+ 'Compiling inference engines...',
+ 'Mapping phoneme sequences...',
+ 'Aligning prosody parameters...',
+ 'Activating speech synthesis...',
+ 'Fine-tuning acoustic models...',
+ 'Preparing voice cloning matrices...',
+ 'Initializing Qwen TTS framework...',
+];
+
function App() {
const [activeTab, setActiveTab] = useState('main');
const [serverReady, setServerReady] = useState(false);
+ const [loadingMessageIndex, setLoadingMessageIndex] = useState(0);
// Setup window close handler and auto-start server when running in Tauri (production only)
useEffect(() => {
@@ -74,21 +99,43 @@ function App() {
};
}, []);
+ // Cycle through loading messages every 3 seconds
+ useEffect(() => {
+ if (!isTauri() || serverReady) {
+ return;
+ }
+
+ const interval = setInterval(() => {
+ setLoadingMessageIndex((prev) => (prev + 1) % LOADING_MESSAGES.length);
+ }, 3000);
+
+ return () => clearInterval(interval);
+ }, [serverReady]);
+
// Show loading screen while server is starting in Tauri
if (isTauri() && !serverReady) {
return (
-
+
+

+
+
+
-
- Starting voicebox...
-
);
diff --git a/app/src/components/ShinyText.tsx b/app/src/components/ShinyText.tsx
new file mode 100644
index 00000000..7291db11
--- /dev/null
+++ b/app/src/components/ShinyText.tsx
@@ -0,0 +1,134 @@
+import { motion, useAnimationFrame, useMotionValue, useTransform } from 'motion/react';
+import type React from 'react';
+import { useCallback, useEffect, useRef, useState } from 'react';
+
+interface ShinyTextProps {
+ text: string;
+ disabled?: boolean;
+ speed?: number;
+ className?: string;
+ color?: string;
+ shineColor?: string;
+ spread?: number;
+ yoyo?: boolean;
+ pauseOnHover?: boolean;
+ direction?: 'left' | 'right';
+ delay?: number;
+}
+
+const ShinyText: React.FC
= ({
+ text,
+ disabled = false,
+ speed = 2,
+ className = '',
+ color = '#b5b5b5',
+ shineColor = '#ffffff',
+ spread = 120,
+ yoyo = false,
+ pauseOnHover = false,
+ direction = 'left',
+ delay = 0,
+}) => {
+ const [isPaused, setIsPaused] = useState(false);
+ const progress = useMotionValue(0);
+ const elapsedRef = useRef(0);
+ const lastTimeRef = useRef(null);
+ const directionRef = useRef(direction === 'left' ? 1 : -1);
+
+ const animationDuration = speed * 1000;
+ const delayDuration = delay * 1000;
+
+ useAnimationFrame((time) => {
+ if (disabled || isPaused) {
+ lastTimeRef.current = null;
+ return;
+ }
+
+ if (lastTimeRef.current === null) {
+ lastTimeRef.current = time;
+ return;
+ }
+
+ const deltaTime = time - lastTimeRef.current;
+ lastTimeRef.current = time;
+
+ elapsedRef.current += deltaTime;
+
+ // Animation goes from 0 to 100
+ if (yoyo) {
+ const cycleDuration = animationDuration + delayDuration;
+ const fullCycle = cycleDuration * 2;
+ const cycleTime = elapsedRef.current % fullCycle;
+
+ if (cycleTime < animationDuration) {
+ // Forward animation: 0 -> 100
+ const p = (cycleTime / animationDuration) * 100;
+ progress.set(directionRef.current === 1 ? p : 100 - p);
+ } else if (cycleTime < cycleDuration) {
+ // Delay at end
+ progress.set(directionRef.current === 1 ? 100 : 0);
+ } else if (cycleTime < cycleDuration + animationDuration) {
+ // Reverse animation: 100 -> 0
+ const reverseTime = cycleTime - cycleDuration;
+ const p = 100 - (reverseTime / animationDuration) * 100;
+ progress.set(directionRef.current === 1 ? p : 100 - p);
+ } else {
+ // Delay at start
+ progress.set(directionRef.current === 1 ? 0 : 100);
+ }
+ } else {
+ const cycleDuration = animationDuration + delayDuration;
+ const cycleTime = elapsedRef.current % cycleDuration;
+
+ if (cycleTime < animationDuration) {
+ // Animation phase: 0 -> 100
+ const p = (cycleTime / animationDuration) * 100;
+ progress.set(directionRef.current === 1 ? p : 100 - p);
+ } else {
+ // Delay phase - hold at end (shine off-screen)
+ progress.set(directionRef.current === 1 ? 100 : 0);
+ }
+ }
+ });
+
+ useEffect(() => {
+ directionRef.current = direction === 'left' ? 1 : -1;
+ elapsedRef.current = 0;
+ progress.set(0);
+ // eslint-d, progress.setisable-next-line react-hooks/exhaustive-deps
+ }, [direction]);
+
+ // Transform: p=0 -> 150% (shine off right), p=100 -> -50% (shine off left)
+ const backgroundPosition = useTransform(progress, (p) => `${150 - p * 2}% center`);
+
+ const handleMouseEnter = useCallback(() => {
+ if (pauseOnHover) setIsPaused(true);
+ }, [pauseOnHover]);
+
+ const handleMouseLeave = useCallback(() => {
+ if (pauseOnHover) setIsPaused(false);
+ }, [pauseOnHover]);
+
+ const gradientStyle: React.CSSProperties = {
+ backgroundImage: `linear-gradient(${spread}deg, ${color} 0%, ${color} 35%, ${shineColor} 50%, ${color} 65%, ${color} 100%)`,
+ backgroundSize: '200% auto',
+ WebkitBackgroundClip: 'text',
+ backgroundClip: 'text',
+ WebkitTextFillColor: 'transparent',
+ };
+
+ return (
+
+ {text}
+
+ );
+};
+
+export default ShinyText;
+// plugins: [],
+// };
diff --git a/bun.lock b/bun.lock
index dfe7bf93..ab6ce78b 100644
--- a/bun.lock
+++ b/bun.lock
@@ -42,6 +42,7 @@
"date-fns": "^3.6.0",
"framer-motion": "^12.29.0",
"lucide-react": "^0.454.0",
+ "motion": "^12.29.0",
"react": "^18.3.0",
"react-dom": "^18.3.0",
"react-hook-form": "^7.53.0",
@@ -859,6 +860,8 @@
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
+ "motion": ["motion@12.29.0", "", { "dependencies": { "framer-motion": "^12.29.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-rjB5CP2N9S2ESAyEFnAFMgTec6X8yvfxLNcz8n12gPq3M48R7ZbBeVYkDOTj8SPMwfvGIFI801SiPSr1+HCr9g=="],
+
"motion-dom": ["motion-dom@12.29.0", "", { "dependencies": { "motion-utils": "^12.27.2" } }, "sha512-3eiz9bb32yvY8Q6XNM4AwkSOBPgU//EIKTZwsSWgA9uzbPBhZJeScCVcBuwwYVqhfamewpv7ZNmVKTGp5qnzkA=="],
"motion-utils": ["motion-utils@12.27.2", "", {}, "sha512-B55gcoL85Mcdt2IEStY5EEAsrMSVE2sI14xQ/uAdPL+mfQxhKKFaEag9JmfxedJOR4vZpBGoPeC/Gm13I/4g5Q=="],
diff --git a/tauri/src-tauri/gen/Assets.car b/tauri/src-tauri/gen/Assets.car
index 8c7ad57d..67fa90c8 100644
Binary files a/tauri/src-tauri/gen/Assets.car and b/tauri/src-tauri/gen/Assets.car differ