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 (
-
+
+
+
+
Voicebox +
+
+
-

- 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