import { useEffect, useState } from 'react'; import { CheckCircle2, Circle, Loader } from 'lucide-react'; interface PipelineStage { id: string; name: string; description: string; status: 'pending' | 'active' | 'completed'; progress?: number; duration?: number; // in seconds startTime?: number; // timestamp when stage started } interface ProcessingPipelineProps { isActive: boolean; currentStage?: string; synthesizerStartTime?: number | null; // timestamp from parent when synthesis started className?: string; } export default function ProcessingPipeline({ isActive, currentStage, synthesizerStartTime, className = "" }: ProcessingPipelineProps) { const [stages, setStages] = useState([ { id: 'encoder', name: 'Speaker Encoder', description: 'Extracting voice embedding', status: 'pending', progress: 0, duration: 3 }, { id: 'synthesizer', name: 'Tacotron2 Synthesizer', description: 'Generating mel-spectrogram', status: 'pending', progress: 0, duration: 45 }, { id: 'vocoder', name: 'WaveRNN Vocoder', description: 'Converting to audio', status: 'pending', progress: 0, duration: 10 } ]); // Real-time sync with backend using elapsed time useEffect(() => { if (!synthesizerStartTime) { return; } // When a new synthesis starts, reset all stages to pending setStages(prev => prev.map(stage => ({ ...stage, status: 'pending', progress: 0 }))); // Update progress based on actual elapsed time from backend const updateProgress = () => { const elapsedMs = Date.now() - synthesizerStartTime; const elapsedSeconds = elapsedMs / 1000; setStages(prevStages => { // If synthesis is no longer active, force all stages to completed if (!isActive) { return prevStages.map(stage => ({ ...stage, status: 'completed', progress: 100 })); } return prevStages.map(stage => { // Define stage timing let stageStart = 0; let stageDuration = stage.duration || 0; if (stage.id === 'encoder') { stageStart = 0; stageDuration = 3; } else if (stage.id === 'synthesizer') { stageStart = 3; stageDuration = 45; } else if (stage.id === 'vocoder') { stageStart = 48; stageDuration = 10; } const stageEnd = stageStart + stageDuration; // Calculate status based on actual elapsed time if (elapsedSeconds < stageStart) { // Stage hasn't started yet return { ...stage, status: 'pending', progress: 0 }; } else if (elapsedSeconds >= stageStart && elapsedSeconds < stageEnd) { // Stage is currently active const stageElapsed = elapsedSeconds - stageStart; const rawProgress = (stageElapsed / stageDuration) * 100; // While synthesis is active, never show a full 100% for any stage const progress = Math.min(99, rawProgress); return { ...stage, status: 'active', progress }; } else { // Stage logically finished, but keep it at 99% until synthesis completes return { ...stage, status: 'active', progress: 99 }; } }); }); }; // Update immediately updateProgress(); // Update every 100ms for smooth animation const interval = setInterval(updateProgress, 100); return () => clearInterval(interval); }, [isActive, synthesizerStartTime]); const getStageIcon = (stage: PipelineStage) => { if (stage.status === 'completed') { return ; } else if (stage.status === 'active') { return ; } else { return ; } }; const getProgressBarColor = (status: string) => { switch (status) { case 'completed': return 'bg-green-500'; case 'active': return 'bg-blue-500'; default: return 'bg-gray-600'; } }; return (

Processing Pipeline

{isActive && ( Synthesizing... )}
{stages.map((stage, index) => (
{/* Stage Header */}
{getStageIcon(stage)}

{stage.name}

{stage.progress !== undefined && stage.status !== 'pending' && ( {Math.round(stage.progress)}% )}

{stage.description}

{/* Progress Bar */}
{/* Connector Line */} {index < stages.length - 1 && (
)}
))}
{/* Timeline Info */}

Speaker Encoder: Loads your voice

Tacotron2: Generates speech pattern

Vocoder: Creates audio waveform

); }