Spaces:
Sleeping
Sleeping
| 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<PipelineStage[]>([ | |
| { | |
| 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 <CheckCircle2 className="w-6 h-6 text-green-400" />; | |
| } else if (stage.status === 'active') { | |
| return <Loader className="w-6 h-6 text-blue-400 animate-spin" />; | |
| } else { | |
| return <Circle className="w-6 h-6 text-gray-500" />; | |
| } | |
| }; | |
| const getProgressBarColor = (status: string) => { | |
| switch (status) { | |
| case 'completed': | |
| return 'bg-green-500'; | |
| case 'active': | |
| return 'bg-blue-500'; | |
| default: | |
| return 'bg-gray-600'; | |
| } | |
| }; | |
| return ( | |
| <div className={`space-y-4 ${className}`}> | |
| <div className="flex items-center justify-between"> | |
| <h3 className="text-sm font-semibold text-foreground"> | |
| Processing Pipeline | |
| </h3> | |
| {isActive && ( | |
| <span className="text-xs text-blue-400 animate-pulse"> | |
| Synthesizing... | |
| </span> | |
| )} | |
| </div> | |
| <div className="space-y-3"> | |
| {stages.map((stage, index) => ( | |
| <div key={stage.id} className="space-y-1.5"> | |
| {/* Stage Header */} | |
| <div className="flex items-center gap-3"> | |
| {getStageIcon(stage)} | |
| <div className="flex-1"> | |
| <div className="flex items-center justify-between"> | |
| <p className="text-sm font-medium text-foreground"> | |
| {stage.name} | |
| </p> | |
| {stage.progress !== undefined && stage.status !== 'pending' && ( | |
| <span className="text-xs text-muted-foreground"> | |
| {Math.round(stage.progress)}% | |
| </span> | |
| )} | |
| </div> | |
| <p className="text-xs text-muted-foreground"> | |
| {stage.description} | |
| </p> | |
| </div> | |
| </div> | |
| {/* Progress Bar */} | |
| <div className="ml-9 h-1.5 bg-surface rounded-full overflow-hidden"> | |
| <div | |
| className={`h-full ${getProgressBarColor(stage.status)} transition-all duration-300 ${ | |
| stage.progress === 100 ? 'w-full' : '' | |
| }`} | |
| data-progress={Math.round(stage.progress || 0)} | |
| /> | |
| </div> | |
| {/* Connector Line */} | |
| {index < stages.length - 1 && ( | |
| <div className="ml-3 h-2 border-l-2 border-gray-600" /> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| {/* Timeline Info */} | |
| <div className="mt-4 p-3 bg-surface rounded-lg border border-border"> | |
| <div className="text-xs text-muted-foreground space-y-1"> | |
| <p className="flex items-center gap-2"> | |
| <CheckCircle2 className="w-3 h-3 text-green-400" /> | |
| <span>Speaker Encoder: Loads your voice</span> | |
| </p> | |
| <p className="flex items-center gap-2"> | |
| <CheckCircle2 className="w-3 h-3 text-green-400" /> | |
| <span>Tacotron2: Generates speech pattern</span> | |
| </p> | |
| <p className="flex items-center gap-2"> | |
| <CheckCircle2 className="w-3 h-3 text-green-400" /> | |
| <span>Vocoder: Creates audio waveform</span> | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |