| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"/> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> |
| <title>Open-Source Voice Agent</title> |
| <style> |
| |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } |
| |
| :root { |
| --bg0: #0f1117; |
| --bg1: #161b27; |
| --bg2: #1e2436; |
| --bg3: #252d42; |
| --border: #2e3a52; |
| --text0: #e8eaf0; |
| --text1: #9aa3b8; |
| --text2: #5c677e; |
| --accent: #4f8ef7; |
| --green: #22c55e; |
| --amber: #f59e0b; |
| --blue: #3b82f6; |
| --red: #ef4444; |
| --radius: 12px; |
| --font: 'Inter', system-ui, -apple-system, sans-serif; |
| } |
| |
| body { |
| background: var(--bg0); |
| color: var(--text0); |
| font-family: var(--font); |
| min-height: 100vh; |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| } |
| |
| |
| .page { |
| width: 100%; |
| max-width: 680px; |
| padding: 24px 16px 48px; |
| display: flex; |
| flex-direction: column; |
| gap: 20px; |
| } |
| |
| |
| .header { text-align: center; padding: 8px 0 4px; } |
| .header h1 { font-size: 22px; font-weight: 600; letter-spacing: -0.3px; } |
| .header p { font-size: 13px; color: var(--text1); margin-top: 6px; } |
| |
| .badges { |
| display: flex; justify-content: center; gap: 8px; flex-wrap: wrap; |
| margin-top: 10px; |
| } |
| .badge { |
| font-size: 11px; font-weight: 500; |
| padding: 3px 10px; |
| border-radius: 20px; |
| border: 1px solid var(--border); |
| color: var(--text1); |
| background: var(--bg2); |
| } |
| .badge span { color: var(--accent); } |
| |
| |
| .card { |
| background: var(--bg1); |
| border: 1px solid var(--border); |
| border-radius: var(--radius); |
| overflow: hidden; |
| } |
| |
| |
| .vis-wrap { |
| padding: 0; |
| background: var(--bg0); |
| border-bottom: 1px solid var(--border); |
| } |
| #visualiser { |
| display: block; |
| width: 100%; |
| height: 90px; |
| } |
| |
| |
| .controls { |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| gap: 14px; |
| padding: 24px 20px 20px; |
| } |
| |
| #btn-toggle { |
| width: 120px; height: 120px; |
| border-radius: 50%; |
| border: 2px solid var(--border); |
| background: var(--bg2); |
| color: var(--text0); |
| font-size: 32px; |
| cursor: pointer; |
| transition: background .15s, transform .1s, border-color .15s; |
| display: flex; align-items: center; justify-content: center; |
| flex-direction: column; |
| gap: 4px; |
| line-height: 1; |
| } |
| #btn-toggle:hover { background: var(--bg3); border-color: var(--accent); } |
| #btn-toggle:active { transform: scale(0.95); } |
| #btn-toggle.active { |
| background: #1a2440; |
| border-color: var(--blue); |
| box-shadow: 0 0 0 4px rgba(79,142,247,0.15); |
| } |
| #btn-label { font-size: 11px; color: var(--text1); font-weight: 500; } |
| |
| |
| .status-bar { |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| padding: 10px 20px; |
| background: var(--bg2); |
| border-top: 1px solid var(--border); |
| justify-content: space-between; |
| } |
| .status-left { display: flex; align-items: center; gap: 8px; } |
| #status-dot { |
| width: 8px; height: 8px; border-radius: 50%; |
| background: #6b7280; |
| transition: background .3s; |
| flex-shrink: 0; |
| } |
| #status-dot.pulse { animation: pulse 1.2s infinite; } |
| @keyframes pulse { |
| 0%, 100% { opacity: 1; } |
| 50% { opacity: 0.4; } |
| } |
| #status-text { font-size: 13px; color: var(--text1); } |
| #latency { font-size: 12px; color: var(--text2); } |
| |
| |
| .transcript-header { |
| padding: 12px 16px 8px; |
| font-size: 11px; |
| font-weight: 600; |
| letter-spacing: .06em; |
| text-transform: uppercase; |
| color: var(--text2); |
| border-bottom: 1px solid var(--border); |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| } |
| #btn-clear { |
| font-size: 11px; color: var(--text2); |
| background: none; border: none; cursor: pointer; |
| padding: 2px 6px; border-radius: 4px; |
| } |
| #btn-clear:hover { color: var(--text1); background: var(--bg3); } |
| |
| #transcript { |
| min-height: 120px; |
| max-height: 320px; |
| overflow-y: auto; |
| padding: 12px 16px; |
| display: flex; |
| flex-direction: column; |
| gap: 10px; |
| scroll-behavior: smooth; |
| } |
| #transcript:empty::before { |
| content: 'Conversation will appear hereβ¦'; |
| color: var(--text2); |
| font-size: 13px; |
| } |
| |
| .msg { display: flex; flex-direction: column; gap: 3px; } |
| .msg-role { |
| font-size: 10px; |
| font-weight: 600; |
| letter-spacing: .05em; |
| text-transform: uppercase; |
| } |
| .msg-text { font-size: 14px; line-height: 1.55; color: var(--text0); } |
| |
| .msg-user .msg-role { color: var(--green); } |
| .msg-agent .msg-role { color: var(--blue); } |
| .msg-user .msg-text { color: var(--text0); } |
| .msg-agent .msg-text { color: var(--text0); } |
| |
| |
| .model-grid { |
| display: grid; |
| grid-template-columns: repeat(3, 1fr); |
| gap: 1px; |
| background: var(--border); |
| border: 1px solid var(--border); |
| border-radius: var(--radius); |
| overflow: hidden; |
| } |
| .model-cell { |
| background: var(--bg1); |
| padding: 12px 14px; |
| text-align: center; |
| } |
| .model-cell .mc-label { |
| font-size: 10px; |
| font-weight: 600; |
| text-transform: uppercase; |
| letter-spacing: .06em; |
| color: var(--text2); |
| margin-bottom: 4px; |
| } |
| .model-cell .mc-name { font-size: 12px; font-weight: 500; color: var(--text0); } |
| .model-cell .mc-size { font-size: 11px; color: var(--text2); margin-top: 2px; } |
| |
| |
| ::-webkit-scrollbar { width: 4px; } |
| ::-webkit-scrollbar-track { background: transparent; } |
| ::-webkit-scrollbar-thumb { background: var(--bg3); border-radius: 2px; } |
| </style> |
| </head> |
| <body> |
| <div class="page"> |
|
|
| |
| <div class="header"> |
| <h1>π€ Open-Source Voice Agent</h1> |
| <p>100% open-source models Β· streaming overlap pipeline Β· English</p> |
| <div class="badges"> |
| <div class="badge"><span>STT</span> Whisper base.en</div> |
| <div class="badge"><span>LLM</span> SmolLM2-1.7B</div> |
| <div class="badge"><span>TTS</span> MMS-TTS-eng</div> |
| <div class="badge"><span>VAD</span> Silero</div> |
| </div> |
| </div> |
|
|
| |
| <div class="card"> |
| |
| <div class="vis-wrap"> |
| <canvas id="visualiser"></canvas> |
| </div> |
|
|
| |
| <div class="controls"> |
| <button id="btn-toggle" aria-label="Start recording"> |
| <span id="btn-icon">π€</span> |
| <span id="btn-label">START</span> |
| </button> |
| </div> |
|
|
| |
| <div class="status-bar"> |
| <div class="status-left"> |
| <div id="status-dot"></div> |
| <span id="status-text">Ready β click Start to begin</span> |
| </div> |
| <span id="latency"></span> |
| </div> |
| </div> |
|
|
| |
| <div class="card"> |
| <div class="transcript-header"> |
| Conversation |
| <button id="btn-clear">Clear</button> |
| </div> |
| <div id="transcript"></div> |
| </div> |
|
|
| |
| <div class="model-grid"> |
| <div class="model-cell"> |
| <div class="mc-label">Speech-to-Text</div> |
| <div class="mc-name">Whisper base.en</div> |
| <div class="mc-size">74 M params Β· GPU/CPU</div> |
| </div> |
| <div class="model-cell"> |
| <div class="mc-label">Language Model</div> |
| <div class="mc-name">SmolLM2-1.7B-Instruct</div> |
| <div class="mc-size">1.7 B params Β· GPU/CPU</div> |
| </div> |
| <div class="model-cell"> |
| <div class="mc-label">Text-to-Speech</div> |
| <div class="mc-name">MMS-TTS-eng</div> |
| <div class="mc-size">VITS Β· 16 kHz Β· CPU</div> |
| </div> |
| </div> |
|
|
| </div> |
|
|
| <script src="/app.js" defer></script> |
| <script> |
| |
| document.getElementById('btn-clear').addEventListener('click', () => { |
| document.getElementById('transcript').innerHTML = ''; |
| }); |
| |
| |
| const _setState_orig = window.setState; |
| |
| const _btnToggle = document.getElementById('btn-toggle'); |
| const _btnIcon = document.getElementById('btn-icon'); |
| const _btnLabel = document.getElementById('btn-label'); |
| const _dot = document.getElementById('status-dot'); |
| |
| |
| const obs = new MutationObserver(() => { |
| const bg = _dot.style.background; |
| if (bg === '#22c55e' || bg === '#3b82f6') { |
| _btnToggle.classList.add('active'); |
| _btnIcon.textContent = 'βΉ'; |
| _btnLabel.textContent = 'STOP'; |
| } else if (bg === '#f59e0b') { |
| _btnToggle.classList.add('active'); |
| _btnIcon.textContent = 'π'; |
| _btnLabel.textContent = 'THINKING'; |
| } else { |
| _btnToggle.classList.remove('active'); |
| _btnIcon.textContent = 'π€'; |
| _btnLabel.textContent = 'START'; |
| } |
| |
| |
| _dot.classList.toggle('pulse', bg !== '#6b7280'); |
| }); |
| obs.observe(_dot, { attributes: true, attributeFilter: ['style'] }); |
| </script> |
| </body> |
| </html> |
|
|