| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { useState, useEffect } from 'react'; |
|
|
| export default function PerformanceMetrics({ |
| latency, |
| rtf, |
| audioDuration, |
| windowState, |
| device, |
| updateInterval, |
| isProcessingFile, |
| fileDuration, |
| transcribedDuration, |
| }) { |
| const [memoryUsage, setMemoryUsage] = useState(null); |
| const [wasProcessingFile, setWasProcessingFile] = useState(false); |
|
|
| |
| useEffect(() => { |
| if (isProcessingFile) { |
| setWasProcessingFile(true); |
| } |
| }, [isProcessingFile]); |
|
|
| useEffect(() => { |
| |
| if (performance.memory) { |
| const interval = setInterval(() => { |
| const memory = performance.memory; |
| setMemoryUsage({ |
| used: (memory.usedJSHeapSize / 1024 / 1024).toFixed(1), |
| total: (memory.totalJSHeapSize / 1024 / 1024).toFixed(1), |
| limit: (memory.jsHeapSizeLimit / 1024 / 1024).toFixed(1), |
| }); |
| }, 1000); |
|
|
| return () => clearInterval(interval); |
| } |
| }, []); |
|
|
| const formatDuration = (seconds) => { |
| if (seconds === null || seconds === undefined) return null; |
| if (seconds < 60) { |
| return seconds.toFixed(1); |
| } |
| const minutes = Math.floor(seconds / 60); |
| const secs = Math.floor(seconds % 60); |
| return `${minutes}m ${secs}s`; |
| }; |
|
|
| const MetricCard = ({ label, value, unit, color = 'gray', formatTime = false }) => { |
| const displayValue = formatTime ? formatDuration(value) : (value !== null && value !== undefined ? value.toFixed(2) : null); |
| const displayUnit = formatTime ? '' : unit; |
|
|
| return ( |
| <div className="bg-gray-800 rounded-lg p-4 border border-gray-700"> |
| <div className="text-xs text-gray-400 uppercase tracking-wider mb-1"> |
| {label} |
| </div> |
| <div className={`text-2xl font-bold text-${color}-400 font-mono`}> |
| {displayValue !== null ? displayValue : '—'} |
| {displayUnit && <span className="text-sm ml-1 text-gray-500">{displayUnit}</span>} |
| </div> |
| </div> |
| ); |
| }; |
|
|
| const getRTFColor = (rtf) => { |
| if (rtf === null || rtf === undefined) return 'gray'; |
| |
| if (rtf > 5) return 'green'; |
| if (rtf > 1) return 'yellow'; |
| return 'red'; |
| }; |
|
|
| const getLatencyColor = (latency) => { |
| if (latency === null || latency === undefined) return 'cyan'; |
| |
| if (latency < 0.5) return 'green'; |
| if (latency < 1.0) return 'yellow'; |
| return 'red'; |
| }; |
|
|
| const getWindowStateIcon = (state) => { |
| if (state === 'growing') return '📈'; |
| if (state === 'sliding') return '↔️'; |
| return '⏸️'; |
| }; |
|
|
| return ( |
| <div className="w-full max-w-4xl mx-auto mt-6"> |
| <div className="bg-gray-900 rounded-lg border border-gray-700 p-6 shadow-xl"> |
| <h2 className="text-xl font-semibold text-gray-100 mb-4 flex items-center gap-2"> |
| <span>📊</span> Performance Metrics |
| </h2> |
| |
| {/* Metrics Grid */} |
| <div className="grid grid-cols-2 md:grid-cols-3 gap-4 mb-4"> |
| <MetricCard |
| label={(isProcessingFile || wasProcessingFile) ? "Processing Time" : "Latency"} |
| value={latency} |
| unit="s" |
| color={(isProcessingFile || wasProcessingFile) ? "blue" : getLatencyColor(latency)} |
| formatTime={true} |
| /> |
| <MetricCard |
| label="Real-time Factor" |
| value={rtf} |
| unit="x" |
| color={getRTFColor(rtf)} |
| /> |
| <MetricCard |
| label={(isProcessingFile || wasProcessingFile) ? "Transcribed" : "Window Size"} |
| value={(isProcessingFile || wasProcessingFile) ? transcribedDuration : audioDuration} |
| unit="s" |
| color="blue" |
| formatTime={true} |
| /> |
| </div> |
| |
| {/* Additional Info */} |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> |
| {/* Window State */} |
| <div className="bg-gray-800 rounded-lg p-4 border border-gray-700"> |
| <div className="text-xs text-gray-400 uppercase tracking-wider mb-1"> |
| Window State |
| </div> |
| <div className="text-lg font-semibold text-gray-200"> |
| {getWindowStateIcon(windowState)} {windowState || 'idle'} |
| </div> |
| <div className="text-xs text-gray-500 mt-1"> |
| {windowState === 'growing' && 'Building context (0-15s)'} |
| {windowState === 'sliding' && 'Sliding window (>15s)'} |
| {!windowState && 'Not recording'} |
| </div> |
| </div> |
| |
| {/* Device */} |
| <div className="bg-gray-800 rounded-lg p-4 border border-gray-700"> |
| <div className="text-xs text-gray-400 uppercase tracking-wider mb-1"> |
| Acceleration |
| </div> |
| <div className="text-lg font-semibold text-gray-200"> |
| {device === 'webgpu-hybrid' && '🚀 WebGPU Hybrid'} |
| {device === 'webgpu' && '🚀 WebGPU'} |
| {device === 'wasm' && '⚙️ WebAssembly'} |
| {device === 'cpu' && '🖥️ CPU'} |
| {!device && '—'} |
| </div> |
| <div className="text-xs text-gray-500 mt-1"> |
| {device === 'webgpu-hybrid' && 'GPU encoder + WASM decoder'} |
| {device === 'webgpu' && 'Hardware accelerated'} |
| {device === 'wasm' && 'Software optimized'} |
| {device === 'cpu' && 'Fallback mode'} |
| </div> |
| </div> |
| |
| {/* Memory (if available) */} |
| {memoryUsage && ( |
| <div className="bg-gray-800 rounded-lg p-4 border border-gray-700"> |
| <div className="text-xs text-gray-400 uppercase tracking-wider mb-1"> |
| Memory Usage |
| </div> |
| <div className="text-lg font-semibold text-gray-200"> |
| {memoryUsage.used} MB |
| </div> |
| <div className="text-xs text-gray-500 mt-1"> |
| of {memoryUsage.total} MB allocated |
| </div> |
| </div> |
| )} |
| </div> |
| |
| {/* RTF Explanation */} |
| {rtf !== null && rtf !== undefined && ( |
| <div className="mt-4 p-3 bg-gray-800 border border-gray-700 rounded text-xs text-gray-400"> |
| <strong>Real-time Factor (RTF):</strong> How many times faster than real-time. |
| {rtf > 1 && ` ✓ ${rtf.toFixed(1)}x faster than real-time`} |
| {rtf <= 1 && ' ⚠️ Slower than real-time'} |
| {' (Higher is better)'} |
| </div> |
| )} |
| </div> |
| |
| {/* Technical Info */} |
| <div className="mt-4 text-xs text-gray-500 text-center space-y-1"> |
| <p>Model: Parakeet TDT 0.6B v3 (ONNX) | Sample Rate: 16kHz</p> |
| <p>Progressive updates every 500ms | Smart window management (15s max)</p> |
| </div> |
| </div> |
| ); |
| } |
|
|