Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | |
| <title>Model Speed Comparator</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet"/> | |
| <style> | |
| :root { | |
| --bg: #0a0a0f; | |
| --surface: #111118; | |
| --surface2: #1a1a24; | |
| --border: #2a2a3a; | |
| --accent: #00e5ff; | |
| --accent2: #7c3aed; | |
| --green: #00ff87; | |
| --yellow: #ffd600; | |
| --red: #ff4757; | |
| --text: #e8e8f0; | |
| --muted: #6b6b80; | |
| --font-mono: 'Space Mono', monospace; | |
| --font-sans: 'DM Sans', sans-serif; | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { | |
| background: var(--bg); | |
| color: var(--text); | |
| font-family: var(--font-sans); | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| } | |
| /* Grid background */ | |
| body::before { | |
| content: ''; | |
| position: fixed; | |
| inset: 0; | |
| background-image: | |
| linear-gradient(rgba(0,229,255,0.03) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(0,229,255,0.03) 1px, transparent 1px); | |
| background-size: 40px 40px; | |
| pointer-events: none; | |
| z-index: 0; | |
| } | |
| .container { | |
| max-width: 960px; | |
| margin: 0 auto; | |
| padding: 48px 24px; | |
| position: relative; | |
| z-index: 1; | |
| } | |
| /* Header */ | |
| header { | |
| margin-bottom: 48px; | |
| } | |
| .tag { | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| color: var(--accent); | |
| letter-spacing: 3px; | |
| text-transform: uppercase; | |
| margin-bottom: 12px; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .tag::before { | |
| content: ''; | |
| width: 24px; | |
| height: 1px; | |
| background: var(--accent); | |
| } | |
| h1 { | |
| font-family: var(--font-mono); | |
| font-size: clamp(28px, 5vw, 44px); | |
| font-weight: 700; | |
| line-height: 1.1; | |
| letter-spacing: -1px; | |
| color: #fff; | |
| } | |
| h1 span { | |
| color: var(--accent); | |
| } | |
| .subtitle { | |
| color: var(--muted); | |
| font-size: 15px; | |
| margin-top: 12px; | |
| font-weight: 300; | |
| max-width: 520px; | |
| line-height: 1.6; | |
| } | |
| /* Input section */ | |
| .input-section { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| padding: 28px; | |
| margin-bottom: 32px; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .input-section::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; left: 0; right: 0; | |
| height: 2px; | |
| background: linear-gradient(90deg, var(--accent), var(--accent2)); | |
| } | |
| .input-label { | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| color: var(--muted); | |
| letter-spacing: 2px; | |
| text-transform: uppercase; | |
| margin-bottom: 12px; | |
| } | |
| textarea { | |
| width: 100%; | |
| background: var(--surface2); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| color: var(--text); | |
| font-family: var(--font-sans); | |
| font-size: 15px; | |
| padding: 16px; | |
| resize: vertical; | |
| min-height: 90px; | |
| transition: border-color 0.2s; | |
| outline: none; | |
| } | |
| textarea:focus { | |
| border-color: var(--accent); | |
| } | |
| .examples { | |
| display: flex; | |
| gap: 8px; | |
| flex-wrap: wrap; | |
| margin-top: 12px; | |
| } | |
| .example-chip { | |
| font-size: 12px; | |
| background: var(--surface2); | |
| border: 1px solid var(--border); | |
| border-radius: 20px; | |
| padding: 4px 12px; | |
| color: var(--muted); | |
| cursor: pointer; | |
| transition: all 0.15s; | |
| font-family: var(--font-mono); | |
| } | |
| .example-chip:hover { | |
| border-color: var(--accent); | |
| color: var(--accent); | |
| } | |
| .action-row { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 12px; | |
| margin-top: 20px; | |
| } | |
| .run-btn { | |
| margin-top: 20px; | |
| background: var(--accent); | |
| color: #000; | |
| border: none; | |
| border-radius: 8px; | |
| padding: 14px 32px; | |
| font-family: var(--font-mono); | |
| font-size: 13px; | |
| font-weight: 700; | |
| letter-spacing: 1px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .action-row .run-btn { | |
| margin-top: 0; | |
| } | |
| .benchmark-btn { | |
| background: transparent; | |
| color: var(--accent); | |
| border: 1px solid var(--accent); | |
| } | |
| .benchmark-btn:hover { | |
| background: rgba(0,229,255,0.08); | |
| color: var(--text); | |
| } | |
| .secondary-btn { | |
| background: var(--surface2); | |
| color: var(--text); | |
| border: 1px solid var(--border); | |
| } | |
| .secondary-btn:hover { | |
| background: rgba(255,255,255,0.04); | |
| color: var(--accent); | |
| border-color: var(--accent); | |
| } | |
| .toggle-active { | |
| background: var(--green); | |
| color: #000; | |
| border-color: var(--green); | |
| } | |
| .run-btn:hover { background: #33eaff; transform: translateY(-1px); } | |
| .run-btn:active { transform: translateY(0); } | |
| .run-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } | |
| /* Spinner */ | |
| .spinner { | |
| width: 16px; height: 16px; | |
| border: 2px solid rgba(0,0,0,0.3); | |
| border-top-color: #000; | |
| border-radius: 50%; | |
| animation: spin 0.7s linear infinite; | |
| display: none; | |
| } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| /* Results */ | |
| #results { display: none; } | |
| .summary-banner { | |
| background: linear-gradient(135deg, rgba(0,229,255,0.08), rgba(124,58,237,0.08)); | |
| border: 1px solid rgba(0,229,255,0.2); | |
| border-radius: 12px; | |
| padding: 20px 24px; | |
| margin-bottom: 24px; | |
| display: flex; | |
| align-items: center; | |
| gap: 16px; | |
| flex-wrap: wrap; | |
| } | |
| .speedup-badge { | |
| font-family: var(--font-mono); | |
| font-size: 36px; | |
| font-weight: 700; | |
| color: var(--green); | |
| line-height: 1; | |
| } | |
| .summary-text { | |
| font-size: 14px; | |
| color: var(--muted); | |
| line-height: 1.6; | |
| } | |
| .summary-text strong { color: var(--text); } | |
| /* Cards grid */ | |
| .cards { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); | |
| gap: 16px; | |
| margin-bottom: 32px; | |
| } | |
| .card { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| padding: 24px; | |
| position: relative; | |
| overflow: hidden; | |
| animation: fadeUp 0.4s ease forwards; | |
| opacity: 0; | |
| } | |
| .card:nth-child(1) { animation-delay: 0.05s; } | |
| .card:nth-child(2) { animation-delay: 0.1s; } | |
| .card:nth-child(3) { animation-delay: 0.15s; } | |
| @keyframes fadeUp { | |
| from { opacity: 0; transform: translateY(16px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .card.winner { | |
| border-color: rgba(0,255,135,0.3); | |
| background: linear-gradient(135deg, var(--surface), rgba(0,255,135,0.04)); | |
| } | |
| .card.winner::before { | |
| content: '⚡ FASTEST'; | |
| position: absolute; | |
| top: 12px; right: 12px; | |
| font-family: var(--font-mono); | |
| font-size: 9px; | |
| letter-spacing: 2px; | |
| color: var(--green); | |
| background: rgba(0,255,135,0.1); | |
| border: 1px solid rgba(0,255,135,0.3); | |
| border-radius: 4px; | |
| padding: 3px 8px; | |
| } | |
| .card-name { | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| letter-spacing: 2px; | |
| text-transform: uppercase; | |
| color: var(--muted); | |
| margin-bottom: 4px; | |
| } | |
| .card-format { | |
| font-size: 13px; | |
| color: var(--accent); | |
| margin-bottom: 20px; | |
| font-weight: 500; | |
| } | |
| .card-label { | |
| font-size: 22px; | |
| font-weight: 600; | |
| margin-bottom: 4px; | |
| color: #fff; | |
| } | |
| .card-confidence { | |
| font-family: var(--font-mono); | |
| font-size: 12px; | |
| color: var(--muted); | |
| margin-bottom: 20px; | |
| } | |
| .metrics { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| } | |
| .metric { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .metric-name { | |
| font-size: 12px; | |
| color: var(--muted); | |
| font-family: var(--font-mono); | |
| } | |
| .metric-value { | |
| font-family: var(--font-mono); | |
| font-size: 14px; | |
| font-weight: 700; | |
| color: var(--text); | |
| } | |
| .bar-wrap { | |
| height: 4px; | |
| background: var(--surface2); | |
| border-radius: 2px; | |
| margin-top: 4px; | |
| overflow: hidden; | |
| } | |
| .bar { | |
| height: 100%; | |
| border-radius: 2px; | |
| transition: width 0.6s cubic-bezier(0.16, 1, 0.3, 1); | |
| background: var(--accent); | |
| } | |
| .bar.size { background: var(--accent2); } | |
| /* Chart section */ | |
| .chart-section { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| padding: 28px; | |
| margin-bottom: 24px; | |
| } | |
| .section-title { | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| letter-spacing: 2px; | |
| text-transform: uppercase; | |
| color: var(--muted); | |
| margin-bottom: 24px; | |
| } | |
| .chart-row { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 16px; | |
| } | |
| .chart-item { | |
| display: grid; | |
| grid-template-columns: 90px 1fr 80px; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .chart-label { | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| color: var(--muted); | |
| text-align: right; | |
| } | |
| .chart-bar-wrap { | |
| height: 28px; | |
| background: var(--surface2); | |
| border-radius: 4px; | |
| overflow: hidden; | |
| } | |
| .chart-bar { | |
| height: 100%; | |
| border-radius: 4px; | |
| display: flex; | |
| align-items: center; | |
| padding-left: 10px; | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| font-weight: 700; | |
| color: #000; | |
| transition: width 0.8s cubic-bezier(0.16, 1, 0.3, 1); | |
| min-width: 0; | |
| white-space: nowrap; | |
| } | |
| .chart-val { | |
| font-family: var(--font-mono); | |
| font-size: 12px; | |
| color: var(--text); | |
| font-weight: 700; | |
| } | |
| .trend-canvas { | |
| width: 100%; | |
| height: 220px; | |
| background: var(--surface2); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| display: block; | |
| } | |
| .stats-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); | |
| gap: 12px; | |
| } | |
| .stat-tile { | |
| background: var(--surface2); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| padding: 14px; | |
| } | |
| .stat-label { | |
| font-family: var(--font-mono); | |
| font-size: 10px; | |
| color: var(--muted); | |
| letter-spacing: 1.5px; | |
| text-transform: uppercase; | |
| margin-bottom: 8px; | |
| } | |
| .stat-value { | |
| font-family: var(--font-mono); | |
| font-size: 18px; | |
| font-weight: 700; | |
| } | |
| .table-wrap { | |
| overflow-x: auto; | |
| } | |
| table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| min-width: 640px; | |
| } | |
| th, td { | |
| border-bottom: 1px solid var(--border); | |
| padding: 12px 10px; | |
| text-align: left; | |
| font-size: 13px; | |
| vertical-align: top; | |
| } | |
| tbody tr:hover { | |
| background: rgba(255,255,255,0.025); | |
| } | |
| th { | |
| font-family: var(--font-mono); | |
| font-size: 10px; | |
| letter-spacing: 1.5px; | |
| text-transform: uppercase; | |
| color: var(--muted); | |
| } | |
| td { | |
| color: var(--text); | |
| } | |
| .mono-cell { | |
| font-family: var(--font-mono); | |
| font-size: 12px; | |
| } | |
| .history-input { | |
| max-width: 320px; | |
| color: var(--muted); | |
| } | |
| .empty-state { | |
| color: var(--muted); | |
| font-size: 13px; | |
| } | |
| /* Explain box */ | |
| .explain-box { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-left: 3px solid var(--accent2); | |
| border-radius: 12px; | |
| padding: 24px 28px; | |
| } | |
| .explain-box h3 { | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| letter-spacing: 2px; | |
| text-transform: uppercase; | |
| color: var(--accent2); | |
| margin-bottom: 16px; | |
| } | |
| .explain-item { | |
| display: flex; | |
| gap: 12px; | |
| margin-bottom: 12px; | |
| font-size: 13px; | |
| line-height: 1.6; | |
| color: var(--muted); | |
| } | |
| .explain-item strong { color: var(--text); } | |
| .explain-dot { | |
| width: 6px; height: 6px; | |
| border-radius: 50%; | |
| background: var(--accent2); | |
| flex-shrink: 0; | |
| margin-top: 7px; | |
| } | |
| /* Error */ | |
| .error-box { | |
| background: rgba(255,71,87,0.08); | |
| border: 1px solid rgba(255,71,87,0.3); | |
| border-radius: 8px; | |
| padding: 16px 20px; | |
| color: var(--red); | |
| font-family: var(--font-mono); | |
| font-size: 13px; | |
| display: none; | |
| margin-top: 16px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <div class="tag">AI Inference Optimization</div> | |
| <h1>Model Speed<br/><span>Comparator</span></h1> | |
| <p class="subtitle">Compare PyTorch baseline vs ONNX vs INT8 Quantized inference — same model, same prediction, dramatically different performance.</p> | |
| </header> | |
| <div class="input-section"> | |
| <div class="input-label">Input Text</div> | |
| <textarea id="inputText" placeholder="Type any sentence to classify as positive or negative...">This product exceeded all my expectations. Absolutely love it!</textarea> | |
| <div class="examples"> | |
| <span class="example-chip" onclick="setExample(this)">The movie was terrible</span> | |
| <span class="example-chip" onclick="setExample(this)">Best experience ever!</span> | |
| <span class="example-chip" onclick="setExample(this)">It was okay, nothing special</span> | |
| <span class="example-chip" onclick="setExample(this)">Completely disappointed</span> | |
| </div> | |
| <div class="action-row"> | |
| <button class="run-btn" id="runBtn" onclick="runComparison()"> | |
| <div class="spinner" id="spinner"></div> | |
| <span id="btnText">RUN COMPARISON</span> | |
| </button> | |
| <button class="run-btn benchmark-btn" id="benchmarkBtn" onclick="runBenchmark()"> | |
| <div class="spinner" id="benchmarkSpinner"></div> | |
| <span id="benchmarkBtnText">RUN 20X BENCHMARK</span> | |
| </button> | |
| <button class="run-btn secondary-btn" id="autoRefreshBtn" onclick="toggleAutoRefresh()">AUTO REFRESH OFF</button> | |
| <button class="run-btn secondary-btn" onclick="downloadHistoryCSV()">DOWNLOAD CSV</button> | |
| </div> | |
| <div class="error-box" id="errorBox"></div> | |
| </div> | |
| <div id="results"> | |
| <div class="summary-banner"> | |
| <div class="speedup-badge" id="speedupBadge">—</div> | |
| <div class="summary-text"> | |
| <strong>Speedup achieved</strong> — Quantized INT8 vs PyTorch baseline<br/> | |
| Smallest model: <strong id="smallestModel">—</strong> · Fastest: <strong id="fastestModel">—</strong> | |
| </div> | |
| <button class="run-btn secondary-btn" id="copyJsonBtn" onclick="copyCurrentResult()">COPY RESULT JSON</button> | |
| </div> | |
| <div class="cards" id="cardsContainer"></div> | |
| <div class="chart-section"> | |
| <div class="section-title">Session Stats</div> | |
| <div class="stats-grid" id="statsGrid"></div> | |
| </div> | |
| <div class="chart-section"> | |
| <div class="section-title">Latency Comparison (ms) — lower is better</div> | |
| <div class="chart-row" id="latencyChart"></div> | |
| </div> | |
| <div class="chart-section"> | |
| <div class="section-title">Live Latency Trend (last 10 runs)</div> | |
| <canvas class="trend-canvas" id="trendCanvas" width="900" height="220"></canvas> | |
| </div> | |
| <div class="chart-section"> | |
| <div class="section-title">Model Size (MB) — lower is better</div> | |
| <div class="chart-row" id="sizeChart"></div> | |
| </div> | |
| <div class="chart-section" id="benchmarkSection" style="display:none"> | |
| <div class="section-title">20-Run Benchmark Latency (ms)</div> | |
| <div class="table-wrap"> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Model</th> | |
| <th>Average</th> | |
| <th>Min</th> | |
| <th>Max</th> | |
| <th>P95</th> | |
| </tr> | |
| </thead> | |
| <tbody id="benchmarkTable"></tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <div class="explain-box"> | |
| <h3>What This Demonstrates</h3> | |
| <div class="explain-item"> | |
| <div class="explain-dot"></div> | |
| <div><strong>ONNX Export</strong> — Converts PyTorch model to a hardware-agnostic format. ONNX Runtime applies graph optimizations (operator fusion, memory planning) that PyTorch doesn't do by default.</div> | |
| </div> | |
| <div class="explain-item"> | |
| <div class="explain-dot"></div> | |
| <div><strong>INT8 Quantization</strong> — Reduces weight precision from 32-bit floats to 8-bit integers. 4x smaller model, faster memory bandwidth, same accuracy on most NLP tasks.</div> | |
| </div> | |
| <div class="explain-item"> | |
| <div class="explain-dot"></div> | |
| <div><strong>Why it matters</strong> — AI accelerator teams (like HCL's) optimize model inference for deployment at scale. These techniques are the foundation of production ML systems.</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="chart-section" id="historySection"> | |
| <div class="section-title">Recent Comparisons</div> | |
| <div class="table-wrap"> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Time</th> | |
| <th>Input</th> | |
| <th>Fastest</th> | |
| <th>Speedup</th> | |
| <th>Baseline</th> | |
| <th>ONNX</th> | |
| <th>Quantized</th> | |
| <th>Copy</th> | |
| </tr> | |
| </thead> | |
| <tbody id="historyTable"></tbody> | |
| </table> | |
| </div> | |
| <div class="empty-state" id="historyEmpty">No comparisons yet.</div> | |
| </div> | |
| </div> | |
| <script> | |
| function setExample(el) { | |
| document.getElementById('inputText').value = el.textContent; | |
| } | |
| const COLORS = { | |
| baseline: '#6b6b80', | |
| onnx: '#00e5ff', | |
| quantized: '#00ff87' | |
| }; | |
| const NAMES = { | |
| baseline: 'BASELINE', | |
| onnx: 'ONNX', | |
| quantized: 'QUANTIZED' | |
| }; | |
| let currentResult = null; | |
| let currentHistory = []; | |
| let autoRefreshTimer = null; | |
| let latencyTrend = []; | |
| function escapeHTML(value) { | |
| return String(value) | |
| .replaceAll('&', '&') | |
| .replaceAll('<', '<') | |
| .replaceAll('>', '>') | |
| .replaceAll('"', '"') | |
| .replaceAll("'", '''); | |
| } | |
| async function parseResponse(res) { | |
| const data = await res.json().catch(() => ({})); | |
| if (!res.ok) { | |
| throw new Error(data.detail || `Server error: ${res.status}`); | |
| } | |
| return data; | |
| } | |
| function setButtonLoading(buttonId, spinnerId, textId, loadingText, idleText, isLoading) { | |
| document.getElementById(buttonId).disabled = isLoading; | |
| document.getElementById(spinnerId).style.display = isLoading ? 'block' : 'none'; | |
| document.getElementById(textId).textContent = isLoading ? loadingText : idleText; | |
| } | |
| function showError(message) { | |
| const errorBox = document.getElementById('errorBox'); | |
| errorBox.textContent = `Error: ${message}`; | |
| errorBox.style.display = 'block'; | |
| } | |
| function clearError() { | |
| document.getElementById('errorBox').style.display = 'none'; | |
| } | |
| async function runComparison() { | |
| const text = document.getElementById('inputText').value.trim(); | |
| if (!text) return; | |
| const btn = document.getElementById('runBtn'); | |
| const spinner = document.getElementById('spinner'); | |
| const btnText = document.getElementById('btnText'); | |
| const errorBox = document.getElementById('errorBox'); | |
| btn.disabled = true; | |
| spinner.style.display = 'block'; | |
| btnText.textContent = 'RUNNING...'; | |
| errorBox.style.display = 'none'; | |
| document.getElementById('results').style.display = 'none'; | |
| try { | |
| const res = await fetch('/compare', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ text }) | |
| }); | |
| if (!res.ok) throw new Error(`Server error: ${res.status}`); | |
| const data = await res.json(); | |
| renderResults(data); | |
| } catch (err) { | |
| errorBox.textContent = `Error: ${err.message}`; | |
| errorBox.style.display = 'block'; | |
| } finally { | |
| btn.disabled = false; | |
| spinner.style.display = 'none'; | |
| btnText.textContent = '▶ RUN COMPARISON'; | |
| } | |
| } | |
| function renderResults(data) { | |
| const { results, summary } = data; | |
| const resultsEl = document.getElementById('results'); | |
| resultsEl.style.display = 'block'; | |
| // Summary banner | |
| document.getElementById('speedupBadge').textContent = `${summary.speedup_vs_baseline}×`; | |
| document.getElementById('smallestModel').textContent = NAMES[summary.smallest]; | |
| document.getElementById('fastestModel').textContent = NAMES[summary.fastest]; | |
| // Cards | |
| const maxLatency = Math.max(...Object.values(results).map(r => r.latency_ms)); | |
| const maxSize = Math.max(...Object.values(results).map(r => r.model_size_mb)); | |
| const cardsEl = document.getElementById('cardsContainer'); | |
| cardsEl.innerHTML = ''; | |
| Object.entries(results).forEach(([key, val]) => { | |
| const isWinner = key === summary.fastest; | |
| const card = document.createElement('div'); | |
| card.className = `card${isWinner ? ' winner' : ''}`; | |
| card.innerHTML = ` | |
| <div class="card-name">${NAMES[key]}</div> | |
| <div class="card-format">${val.format}</div> | |
| <div class="card-label">${val.label}</div> | |
| <div class="card-confidence">confidence: ${(val.confidence * 100).toFixed(1)}%</div> | |
| <div class="metrics"> | |
| <div class="metric"> | |
| <span class="metric-name">LATENCY</span> | |
| <span class="metric-value" style="color:${COLORS[key]}">${val.latency_ms}ms</span> | |
| </div> | |
| <div class="bar-wrap"><div class="bar" style="width:${(val.latency_ms/maxLatency)*100}%;background:${COLORS[key]}"></div></div> | |
| <div class="metric" style="margin-top:8px"> | |
| <span class="metric-name">MODEL SIZE</span> | |
| <span class="metric-value">${val.model_size_mb} MB</span> | |
| </div> | |
| <div class="bar-wrap"><div class="bar size" style="width:${(val.model_size_mb/maxSize)*100}%"></div></div> | |
| </div>`; | |
| cardsEl.appendChild(card); | |
| }); | |
| // Charts | |
| renderBarChart('latencyChart', results, 'latency_ms', 'ms', maxLatency); | |
| renderBarChart('sizeChart', results, 'model_size_mb', 'MB', maxSize); | |
| } | |
| function renderBarChart(elId, results, field, unit, maxVal) { | |
| const el = document.getElementById(elId); | |
| el.innerHTML = ''; | |
| Object.entries(results).forEach(([key, val]) => { | |
| const pct = Math.max(4, (val[field] / maxVal) * 100); | |
| const row = document.createElement('div'); | |
| row.className = 'chart-item'; | |
| row.innerHTML = ` | |
| <div class="chart-label">${NAMES[key]}</div> | |
| <div class="chart-bar-wrap"> | |
| <div class="chart-bar" style="width:${pct}%;background:${COLORS[key]}"> | |
| ${val[field]}${unit} | |
| </div> | |
| </div> | |
| <div class="chart-val">${val[field]} ${unit}</div>`; | |
| el.appendChild(row); | |
| }); | |
| } | |
| async function runComparison() { | |
| const text = document.getElementById('inputText').value.trim(); | |
| if (!text) return; | |
| setButtonLoading('runBtn', 'spinner', 'btnText', 'RUNNING...', 'RUN COMPARISON', true); | |
| clearError(); | |
| document.getElementById('results').style.display = 'none'; | |
| document.getElementById('benchmarkSection').style.display = 'none'; | |
| try { | |
| const res = await fetch('/compare', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ text }) | |
| }); | |
| const data = await parseResponse(res); | |
| renderResults(data); | |
| await loadHistory(); | |
| } catch (err) { | |
| showError(err.message); | |
| } finally { | |
| setButtonLoading('runBtn', 'spinner', 'btnText', 'RUNNING...', 'RUN COMPARISON', false); | |
| } | |
| } | |
| async function runBenchmark() { | |
| const text = document.getElementById('inputText').value.trim(); | |
| if (!text) return; | |
| setButtonLoading('benchmarkBtn', 'benchmarkSpinner', 'benchmarkBtnText', 'BENCHMARKING...', 'RUN 20X BENCHMARK', true); | |
| clearError(); | |
| try { | |
| const res = await fetch('/benchmark', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ text }) | |
| }); | |
| const data = await parseResponse(res); | |
| renderResults({ results: data.latest_results, summary: data.summary }); | |
| renderBenchmark(data); | |
| } catch (err) { | |
| showError(err.message); | |
| } finally { | |
| setButtonLoading('benchmarkBtn', 'benchmarkSpinner', 'benchmarkBtnText', 'BENCHMARKING...', 'RUN 20X BENCHMARK', false); | |
| } | |
| } | |
| function renderBenchmark(data) { | |
| document.getElementById('results').style.display = 'block'; | |
| document.getElementById('benchmarkSection').style.display = 'block'; | |
| const table = document.getElementById('benchmarkTable'); | |
| table.innerHTML = ''; | |
| Object.entries(data.results).forEach(([key, val]) => { | |
| const row = document.createElement('tr'); | |
| row.innerHTML = ` | |
| <td class="mono-cell" style="color:${COLORS[key]}">${NAMES[key]}</td> | |
| <td class="mono-cell">${val.avg_latency_ms} ms</td> | |
| <td class="mono-cell">${val.min_latency_ms} ms</td> | |
| <td class="mono-cell">${val.max_latency_ms} ms</td> | |
| <td class="mono-cell">${val.p95_latency_ms} ms</td>`; | |
| table.appendChild(row); | |
| }); | |
| } | |
| async function loadHistory() { | |
| try { | |
| const res = await fetch('/history'); | |
| const data = await parseResponse(res); | |
| renderHistory(data.history || []); | |
| } catch (err) { | |
| renderHistory([]); | |
| } | |
| } | |
| function renderHistory(history) { | |
| const table = document.getElementById('historyTable'); | |
| const empty = document.getElementById('historyEmpty'); | |
| table.innerHTML = ''; | |
| empty.style.display = history.length ? 'none' : 'block'; | |
| history.forEach(item => { | |
| const row = document.createElement('tr'); | |
| const time = new Date(item.timestamp).toLocaleTimeString([], { | |
| hour: '2-digit', | |
| minute: '2-digit', | |
| second: '2-digit' | |
| }); | |
| row.innerHTML = ` | |
| <td class="mono-cell">${time}</td> | |
| <td class="history-input">${escapeHTML(item.input)}</td> | |
| <td class="mono-cell">${NAMES[item.summary.fastest]}</td> | |
| <td class="mono-cell">${item.summary.speedup_vs_baseline}x</td> | |
| <td class="mono-cell">${item.results.baseline.latency_ms} ms</td> | |
| <td class="mono-cell">${item.results.onnx.latency_ms} ms</td> | |
| <td class="mono-cell">${item.results.quantized.latency_ms} ms</td>`; | |
| table.appendChild(row); | |
| }); | |
| } | |
| async function runComparison() { | |
| const text = document.getElementById('inputText').value.trim(); | |
| if (!text) return; | |
| setButtonLoading('runBtn', 'spinner', 'btnText', 'RUNNING...', 'RUN COMPARISON', true); | |
| clearError(); | |
| document.getElementById('results').style.display = 'none'; | |
| document.getElementById('benchmarkSection').style.display = 'none'; | |
| try { | |
| const res = await fetch('/compare', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ text }) | |
| }); | |
| const data = await parseResponse(res); | |
| renderResults(data); | |
| addTrendPoint(data.results); | |
| await Promise.all([loadHistory(), loadStats()]); | |
| } catch (err) { | |
| showError(err.message); | |
| } finally { | |
| setButtonLoading('runBtn', 'spinner', 'btnText', 'RUNNING...', 'RUN COMPARISON', false); | |
| } | |
| } | |
| function renderResults(data) { | |
| const { results, summary } = data; | |
| currentResult = data; | |
| document.getElementById('results').style.display = 'block'; | |
| document.getElementById('speedupBadge').textContent = `${summary.speedup_vs_baseline}x`; | |
| document.getElementById('smallestModel').textContent = NAMES[summary.smallest]; | |
| document.getElementById('fastestModel').textContent = NAMES[summary.fastest]; | |
| const maxLatency = Math.max(...Object.values(results).map(r => r.latency_ms)); | |
| const maxSize = Math.max(...Object.values(results).map(r => r.model_size_mb)); | |
| const cardsEl = document.getElementById('cardsContainer'); | |
| cardsEl.innerHTML = ''; | |
| Object.entries(results).forEach(([key, val]) => { | |
| const isWinner = key === summary.fastest; | |
| const card = document.createElement('div'); | |
| card.className = `card${isWinner ? ' winner' : ''}`; | |
| card.innerHTML = ` | |
| <div class="card-name">${NAMES[key]}</div> | |
| <div class="card-format">${escapeHTML(val.format)}</div> | |
| <div class="card-label">${escapeHTML(val.label)}</div> | |
| <div class="card-confidence">confidence: ${(val.confidence * 100).toFixed(1)}%</div> | |
| <div class="metrics"> | |
| <div class="metric"> | |
| <span class="metric-name">LATENCY</span> | |
| <span class="metric-value" style="color:${COLORS[key]}">${val.latency_ms}ms</span> | |
| </div> | |
| <div class="bar-wrap"><div class="bar" style="width:${(val.latency_ms/maxLatency)*100}%;background:${COLORS[key]}"></div></div> | |
| <div class="metric" style="margin-top:8px"> | |
| <span class="metric-name">MODEL SIZE</span> | |
| <span class="metric-value">${val.model_size_mb} MB</span> | |
| </div> | |
| <div class="bar-wrap"><div class="bar size" style="width:${(val.model_size_mb/maxSize)*100}%"></div></div> | |
| </div>`; | |
| cardsEl.appendChild(card); | |
| }); | |
| renderBarChart('latencyChart', results, 'latency_ms', 'ms', maxLatency); | |
| renderBarChart('sizeChart', results, 'model_size_mb', 'MB', maxSize); | |
| } | |
| async function runBenchmark() { | |
| const text = document.getElementById('inputText').value.trim(); | |
| if (!text) return; | |
| setButtonLoading('benchmarkBtn', 'benchmarkSpinner', 'benchmarkBtnText', 'BENCHMARKING...', 'RUN 20X BENCHMARK', true); | |
| clearError(); | |
| try { | |
| const res = await fetch('/benchmark', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ text }) | |
| }); | |
| const data = await parseResponse(res); | |
| renderResults({ input: data.input, results: data.latest_results, summary: data.summary }); | |
| renderBenchmark(data); | |
| } catch (err) { | |
| showError(err.message); | |
| } finally { | |
| setButtonLoading('benchmarkBtn', 'benchmarkSpinner', 'benchmarkBtnText', 'BENCHMARKING...', 'RUN 20X BENCHMARK', false); | |
| } | |
| } | |
| async function loadStats() { | |
| const res = await fetch('/stats'); | |
| const stats = await parseResponse(res); | |
| renderStats(stats); | |
| } | |
| function renderStats(stats) { | |
| const grid = document.getElementById('statsGrid'); | |
| grid.innerHTML = ` | |
| <div class="stat-tile"> | |
| <div class="stat-label">Total Requests</div> | |
| <div class="stat-value">${stats.total_requests}</div> | |
| </div> | |
| ${Object.entries(stats.avg_latency).map(([key, value]) => ` | |
| <div class="stat-tile"> | |
| <div class="stat-label">${NAMES[key]} Avg</div> | |
| <div class="stat-value" style="color:${COLORS[key]}">${value} ms</div> | |
| </div> | |
| `).join('')} | |
| ${Object.entries(stats.fastest_count).map(([key, value]) => ` | |
| <div class="stat-tile"> | |
| <div class="stat-label">${NAMES[key]} Wins</div> | |
| <div class="stat-value">${value}</div> | |
| </div> | |
| `).join('')}`; | |
| } | |
| function addTrendPoint(results) { | |
| latencyTrend.push({ | |
| baseline: results.baseline.latency_ms, | |
| onnx: results.onnx.latency_ms, | |
| quantized: results.quantized.latency_ms | |
| }); | |
| latencyTrend = latencyTrend.slice(-10); | |
| drawTrend(); | |
| } | |
| function drawTrend() { | |
| const canvas = document.getElementById('trendCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const width = canvas.width; | |
| const height = canvas.height; | |
| const padding = 34; | |
| ctx.clearRect(0, 0, width, height); | |
| ctx.fillStyle = '#1a1a24'; | |
| ctx.fillRect(0, 0, width, height); | |
| if (!latencyTrend.length) { | |
| ctx.fillStyle = '#6b6b80'; | |
| ctx.font = '13px monospace'; | |
| ctx.fillText('Run comparisons to build a live latency trend.', padding, height / 2); | |
| return; | |
| } | |
| const allValues = latencyTrend.flatMap(point => Object.values(point)); | |
| const maxValue = Math.max(...allValues, 1); | |
| ctx.strokeStyle = '#2a2a3a'; | |
| ctx.lineWidth = 1; | |
| for (let i = 0; i < 4; i++) { | |
| const y = padding + ((height - padding * 2) / 3) * i; | |
| ctx.beginPath(); | |
| ctx.moveTo(padding, y); | |
| ctx.lineTo(width - padding, y); | |
| ctx.stroke(); | |
| } | |
| Object.keys(COLORS).forEach(modelName => { | |
| ctx.strokeStyle = COLORS[modelName]; | |
| ctx.lineWidth = 2; | |
| ctx.beginPath(); | |
| latencyTrend.forEach((point, index) => { | |
| const x = padding + (index / Math.max(latencyTrend.length - 1, 1)) * (width - padding * 2); | |
| const y = height - padding - (point[modelName] / maxValue) * (height - padding * 2); | |
| if (index === 0) ctx.moveTo(x, y); | |
| else ctx.lineTo(x, y); | |
| }); | |
| ctx.stroke(); | |
| }); | |
| } | |
| async function toggleAutoRefresh() { | |
| const btn = document.getElementById('autoRefreshBtn'); | |
| if (autoRefreshTimer) { | |
| clearInterval(autoRefreshTimer); | |
| autoRefreshTimer = null; | |
| btn.textContent = 'AUTO REFRESH OFF'; | |
| btn.classList.remove('toggle-active'); | |
| return; | |
| } | |
| btn.textContent = 'AUTO REFRESH ON'; | |
| btn.classList.add('toggle-active'); | |
| await runComparison(); | |
| autoRefreshTimer = setInterval(runComparison, 10000); | |
| } | |
| function downloadHistoryCSV() { | |
| const rows = [ | |
| ['timestamp', 'input text', 'baseline ms', 'onnx ms', 'quantized ms'], | |
| ...currentHistory.map(item => [ | |
| item.timestamp, | |
| item.input, | |
| item.results.baseline.latency_ms, | |
| item.results.onnx.latency_ms, | |
| item.results.quantized.latency_ms | |
| ]) | |
| ]; | |
| const csv = rows.map(row => row.map(value => `"${String(value).replaceAll('"', '""')}"`).join(',')).join('\n'); | |
| const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); | |
| const url = URL.createObjectURL(blob); | |
| const link = document.createElement('a'); | |
| link.href = url; | |
| link.download = 'model-speed-history.csv'; | |
| link.click(); | |
| URL.revokeObjectURL(url); | |
| } | |
| async function copyCurrentResult() { | |
| if (!currentResult) return; | |
| await copyJSON(currentResult); | |
| } | |
| async function copyJSON(value) { | |
| const json = JSON.stringify(value, null, 2); | |
| if (navigator.clipboard) { | |
| await navigator.clipboard.writeText(json); | |
| } else { | |
| const textarea = document.createElement('textarea'); | |
| textarea.value = json; | |
| document.body.appendChild(textarea); | |
| textarea.select(); | |
| document.execCommand('copy'); | |
| textarea.remove(); | |
| } | |
| } | |
| function renderHistory(history) { | |
| currentHistory = history; | |
| const table = document.getElementById('historyTable'); | |
| const empty = document.getElementById('historyEmpty'); | |
| table.innerHTML = ''; | |
| empty.style.display = history.length ? 'none' : 'block'; | |
| history.forEach((item, index) => { | |
| const row = document.createElement('tr'); | |
| const time = new Date(item.timestamp).toLocaleTimeString([], { | |
| hour: '2-digit', | |
| minute: '2-digit', | |
| second: '2-digit' | |
| }); | |
| row.innerHTML = ` | |
| <td class="mono-cell">${time}</td> | |
| <td class="history-input">${escapeHTML(item.input)}</td> | |
| <td class="mono-cell">${NAMES[item.summary.fastest]}</td> | |
| <td class="mono-cell">${item.summary.speedup_vs_baseline}x</td> | |
| <td class="mono-cell">${item.results.baseline.latency_ms} ms</td> | |
| <td class="mono-cell">${item.results.onnx.latency_ms} ms</td> | |
| <td class="mono-cell">${item.results.quantized.latency_ms} ms</td> | |
| <td><button class="run-btn secondary-btn" style="padding:8px 10px;font-size:10px" onclick="copyJSON(currentHistory[${index}])">JSON</button></td>`; | |
| table.appendChild(row); | |
| }); | |
| } | |
| // Allow Enter key to submit | |
| document.getElementById('inputText').addEventListener('keydown', e => { | |
| if (e.key === 'Enter' && e.ctrlKey) runComparison(); | |
| }); | |
| loadHistory(); | |
| loadStats(); | |
| drawTrend(); | |
| </script> | |
| </body> | |
| </html> | |