Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta | |
| name="viewport" | |
| content="width=device-width, initial-scale=1, maximum-scale=1" | |
| /> | |
| <title>Open Voice Agent</title> | |
| <style> | |
| *, | |
| *::before, | |
| *::after { | |
| box-sizing: border-box; | |
| } | |
| :root { | |
| --bg-primary: #0b0e13; | |
| --bg-secondary: #131720; | |
| --bg-tertiary: #1a2030; | |
| --bg-card: #161c2a; | |
| --border: #252d3f; | |
| --border-light: #2e3a50; | |
| --text-primary: #eef0f6; | |
| --text-secondary: #8d95ab; | |
| --text-muted: #5c6478; | |
| --accent: #6c8fff; | |
| --accent-light: #8eaaff; | |
| --accent-glow: rgba(108, 143, 255, 0.15); | |
| --accent-gradient: linear-gradient(135deg, #6c8fff 0%, #a78bfa 100%); | |
| --user-color: #22c55e; | |
| --user-bg: rgba(34, 197, 94, 0.08); | |
| --agent-color: #6c8fff; | |
| --agent-bg: rgba(108, 143, 255, 0.08); | |
| --warning: #f59e0b; | |
| --critical: #ef4444; | |
| --radius: 12px; | |
| --radius-sm: 8px; | |
| --radius-xs: 6px; | |
| } | |
| body { | |
| font-family: 'Inter', system-ui, -apple-system, sans-serif; | |
| margin: 0; | |
| padding: 0; | |
| background: var(--bg-primary); | |
| color: var(--text-primary); | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| } | |
| /* Header */ | |
| .header { | |
| padding: 16px 24px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| border-bottom: 1px solid var(--border); | |
| background: var(--bg-secondary); | |
| } | |
| .header-left { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .logo { | |
| font-size: 18px; | |
| font-weight: 700; | |
| letter-spacing: -0.3px; | |
| background: var(--accent-gradient); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .status-badge { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 4px 10px; | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border); | |
| border-radius: 20px; | |
| font-size: 12px; | |
| color: var(--text-secondary); | |
| } | |
| .meta-badge { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 4px 10px; | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border); | |
| border-radius: 20px; | |
| font-size: 11px; | |
| color: var(--text-secondary); | |
| } | |
| .meta-label { | |
| color: var(--text-muted); | |
| font-weight: 600; | |
| } | |
| .meta-value { | |
| color: var(--text-primary); | |
| font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; | |
| font-size: 10px; | |
| } | |
| .status-dot { | |
| width: 7px; | |
| height: 7px; | |
| border-radius: 50%; | |
| background: var(--text-muted); | |
| transition: background 0.3s; | |
| } | |
| .status-dot.connected { | |
| background: #22c55e; | |
| box-shadow: 0 0 6px rgba(34, 197, 94, 0.5); | |
| } | |
| .status-dot.connecting { | |
| background: var(--warning); | |
| animation: pulse-dot 1s infinite; | |
| } | |
| @keyframes pulse-dot { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.4; } | |
| } | |
| .controls { | |
| display: flex; | |
| gap: 6px; | |
| } | |
| .controls button { | |
| padding: 7px 14px; | |
| border-radius: 20px; | |
| border: 1px solid var(--border); | |
| background: var(--bg-tertiary); | |
| color: var(--text-primary); | |
| cursor: pointer; | |
| font-size: 12px; | |
| font-weight: 500; | |
| transition: all 0.2s; | |
| display: flex; | |
| align-items: center; | |
| gap: 5px; | |
| } | |
| .controls button:hover:not(:disabled) { | |
| background: var(--bg-card); | |
| border-color: var(--border-light); | |
| } | |
| .controls button:disabled { | |
| opacity: 0.35; | |
| cursor: not-allowed; | |
| } | |
| .controls button.btn-primary { | |
| background: var(--accent); | |
| border-color: var(--accent); | |
| color: #fff; | |
| } | |
| .controls button.btn-primary:hover:not(:disabled) { | |
| background: var(--accent-light); | |
| border-color: var(--accent-light); | |
| } | |
| .controls button.btn-danger { | |
| color: var(--critical); | |
| border-color: rgba(239, 68, 68, 0.3); | |
| } | |
| .controls button.btn-danger:hover:not(:disabled) { | |
| background: rgba(239, 68, 68, 0.1); | |
| border-color: rgba(239, 68, 68, 0.5); | |
| } | |
| /* Main layout — single column, top to bottom */ | |
| .main { | |
| display: flex; | |
| flex-direction: column; | |
| height: calc(100vh - 57px); | |
| overflow-x: hidden; | |
| overflow-y: auto; | |
| } | |
| /* Waveform hero */ | |
| .waveform-section { | |
| flex-grow: 1; | |
| flex-shrink: 1; | |
| min-height: 200px; | |
| max-height: 60vh; | |
| padding: 20px 24px 16px; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| background: radial-gradient(ellipse at center, rgba(108, 143, 255, 0.04) 0%, transparent 70%); | |
| overflow: hidden; | |
| } | |
| canvas { | |
| width: 100%; | |
| max-width: 900px; | |
| height: auto; | |
| max-height: 100%; | |
| border-radius: var(--radius-sm); | |
| } | |
| /* Live metrics row */ | |
| .live-metrics-row { | |
| flex-shrink: 0; | |
| padding: 0 24px 12px; | |
| } | |
| .pipeline-header { | |
| display: flex; | |
| align-items: baseline; | |
| gap: 10px; | |
| margin-bottom: 10px; | |
| } | |
| .pipeline-title { | |
| font-size: 11px; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 0.08em; | |
| color: var(--text-muted); | |
| } | |
| .pipeline-subtitle { | |
| font-size: 11px; | |
| color: var(--text-muted); | |
| } | |
| .pipeline-stage-row { | |
| display: grid; | |
| grid-template-columns: repeat(3, minmax(0, 1fr)); | |
| gap: 10px; | |
| } | |
| .pipeline-stage-row.handoff-visible { | |
| grid-template-columns: repeat(4, minmax(0, 1fr)); | |
| } | |
| .stage-card[hidden] { | |
| display: none; | |
| } | |
| .metric-card { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| padding: 10px 12px; | |
| } | |
| .stage-card { | |
| position: relative; | |
| } | |
| .stage-card::after { | |
| content: ""; | |
| position: absolute; | |
| top: 50%; | |
| left: calc(100% - 6px); | |
| width: 16px; | |
| height: 1px; | |
| border-radius: 999px; | |
| background: linear-gradient( | |
| 90deg, | |
| rgba(108, 143, 255, 0.18) 0%, | |
| rgba(108, 143, 255, 0.5) 60%, | |
| rgba(167, 139, 250, 0.58) 100% | |
| ); | |
| box-shadow: 0 0 5px rgba(108, 143, 255, 0.18); | |
| transform: translateY(-50%); | |
| pointer-events: none; | |
| z-index: 1; | |
| } | |
| .stage-card::before { | |
| content: ""; | |
| position: absolute; | |
| top: 50%; | |
| left: calc(100% + 10px); | |
| width: 6px; | |
| height: 6px; | |
| background: rgba(167, 139, 250, 0.72); | |
| clip-path: polygon(0 0, 100% 50%, 0 100%); | |
| box-shadow: 0 0 6px rgba(108, 143, 255, 0.2); | |
| transform: translateY(-50%); | |
| pointer-events: none; | |
| z-index: 1; | |
| } | |
| .stage-card:last-child::after { | |
| display: none; | |
| } | |
| .stage-card:last-child::before { | |
| display: none; | |
| } | |
| .metric-card-top { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 8px; | |
| } | |
| .metric-step { | |
| width: 22px; | |
| height: 22px; | |
| border-radius: 6px; | |
| display: inline-grid; | |
| place-items: center; | |
| font-size: 11px; | |
| font-weight: 700; | |
| color: var(--accent-light); | |
| background: var(--accent-glow); | |
| border: 1px solid rgba(108, 143, 255, 0.35); | |
| flex-shrink: 0; | |
| } | |
| .metric-tech-wrap { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .metric-tech { | |
| font-size: 10px; | |
| color: var(--text-secondary); | |
| } | |
| .metric-card-human { | |
| font-size: 13px; | |
| font-weight: 600; | |
| margin-bottom: 2px; | |
| } | |
| .metric-card-desc { | |
| font-size: 11px; | |
| color: var(--text-muted); | |
| margin-bottom: 8px; | |
| min-height: 28px; | |
| } | |
| .metric-card-value-row { | |
| display: flex; | |
| align-items: baseline; | |
| gap: 8px; | |
| margin-bottom: 8px; | |
| } | |
| .metric-card-value { | |
| font-size: 22px; | |
| font-weight: 700; | |
| line-height: 1; | |
| font-variant-numeric: tabular-nums; | |
| color: var(--accent-light); | |
| transition: color 0.3s; | |
| } | |
| .metric-card-value.loading { | |
| font-size: 13px; | |
| font-weight: 600; | |
| line-height: 1.2; | |
| color: var(--text-muted); | |
| } | |
| .metric-card-value.warning { color: var(--warning); } | |
| .metric-card-value.critical { color: var(--critical); } | |
| .metric-card-avg { | |
| font-size: 10px; | |
| color: var(--text-secondary); | |
| } | |
| .metric-card-track { | |
| height: 4px; | |
| background: var(--border); | |
| border-radius: 999px; | |
| overflow: hidden; | |
| } | |
| .metric-card-fill { | |
| height: 100%; | |
| border-radius: 999px; | |
| background: var(--accent-gradient); | |
| transition: width 0.5s ease, background 0.3s; | |
| } | |
| .metric-card-fill.warning { background: var(--warning); } | |
| .metric-card-fill.critical { background: var(--critical); } | |
| .pipeline-total-card { | |
| margin-top: 10px; | |
| background: linear-gradient(135deg, rgba(108, 143, 255, 0.08), rgba(167, 139, 250, 0.06)); | |
| border: 1px solid rgba(108, 143, 255, 0.28); | |
| border-radius: var(--radius-sm); | |
| padding: 12px 14px; | |
| } | |
| .pipeline-total-top { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: flex-end; | |
| gap: 10px; | |
| margin-bottom: 10px; | |
| } | |
| .pipeline-total-label { | |
| font-size: 14px; | |
| font-weight: 600; | |
| } | |
| .pipeline-total-tech { | |
| font-size: 10px; | |
| color: var(--text-secondary); | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .pipeline-total-value-wrap { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: flex-end; | |
| gap: 2px; | |
| } | |
| .pipeline-total-value { | |
| font-size: 28px; | |
| } | |
| .pipeline-total-avg { | |
| font-size: 11px; | |
| color: var(--text-secondary); | |
| } | |
| .tooltip { | |
| position: relative; | |
| display: inline-flex; | |
| align-items: center; | |
| } | |
| .tooltip-trigger { | |
| width: 16px; | |
| height: 16px; | |
| padding: 0; | |
| border-radius: 50%; | |
| border: 1px solid var(--border-light); | |
| background: rgba(255, 255, 255, 0.02); | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| font-size: 10px; | |
| line-height: 1; | |
| display: inline-grid; | |
| place-items: center; | |
| } | |
| .tooltip-trigger:hover { | |
| border-color: rgba(108, 143, 255, 0.4); | |
| color: var(--text-primary); | |
| } | |
| .tooltip-content { | |
| position: absolute; | |
| right: 0; | |
| bottom: calc(100% + 8px); | |
| width: 220px; | |
| padding: 8px; | |
| border-radius: 8px; | |
| border: 1px solid var(--border-light); | |
| background: #111725; | |
| color: var(--text-primary); | |
| font-size: 11px; | |
| line-height: 1.35; | |
| opacity: 0; | |
| pointer-events: none; | |
| transform: translateY(4px); | |
| transition: opacity 0.2s ease, transform 0.2s ease; | |
| z-index: 20; | |
| white-space: normal; | |
| overflow-wrap: anywhere; | |
| } | |
| .tooltip.tooltip-right .tooltip-content { | |
| left: calc(100% + 8px); | |
| right: auto; | |
| top: 50%; | |
| bottom: auto; | |
| transform: translateY(-50%) translateX(4px); | |
| } | |
| .tooltip:hover .tooltip-content, | |
| .tooltip:focus-within .tooltip-content { | |
| opacity: 1; | |
| pointer-events: auto; | |
| transform: translateY(0); | |
| } | |
| .tooltip.tooltip-right:hover .tooltip-content, | |
| .tooltip.tooltip-right:focus-within .tooltip-content { | |
| transform: translateY(-50%) translateX(0); | |
| } | |
| @media (max-width: 1100px) { | |
| .pipeline-stage-row { | |
| grid-template-columns: repeat(2, minmax(0, 1fr)); | |
| } | |
| .stage-card::after { | |
| display: none; | |
| } | |
| .stage-card::before { | |
| display: none; | |
| } | |
| .stage-card:nth-child(odd)::after { | |
| display: block; | |
| } | |
| .stage-card:nth-child(odd)::before { | |
| display: block; | |
| } | |
| .stage-card:last-child::after { | |
| display: none; | |
| } | |
| .stage-card:last-child::before { | |
| display: none; | |
| } | |
| } | |
| @media (max-width: 700px) { | |
| .waveform-section { | |
| min-height: 160px; | |
| padding: 14px 14px 10px; | |
| } | |
| .live-metrics-row { | |
| padding: 0 14px 10px; | |
| } | |
| .pipeline-stage-row { | |
| grid-template-columns: 1fr; | |
| } | |
| .pipeline-total-top { | |
| align-items: flex-start; | |
| flex-direction: column; | |
| } | |
| .pipeline-total-value-wrap { | |
| align-items: flex-start; | |
| } | |
| .stage-card::after { | |
| display: none ; | |
| } | |
| .stage-card::before { | |
| display: none ; | |
| } | |
| .tooltip.tooltip-right .tooltip-content { | |
| left: auto; | |
| right: 0; | |
| top: auto; | |
| bottom: calc(100% + 8px); | |
| transform: translateY(4px); | |
| } | |
| .tooltip.tooltip-right:hover .tooltip-content, | |
| .tooltip.tooltip-right:focus-within .tooltip-content { | |
| transform: translateY(0); | |
| } | |
| } | |
| /* Footer note */ | |
| .footer-note { | |
| flex-shrink: 0; | |
| padding: 8px 24px; | |
| border-top: 1px solid var(--border); | |
| font-size: 10px; | |
| color: var(--text-muted); | |
| text-align: center; | |
| background: var(--bg-secondary); | |
| } | |
| .footer-note a { | |
| color: var(--accent); | |
| text-decoration: none; | |
| transition: color 0.2s; | |
| } | |
| .footer-note a:hover { | |
| color: var(--accent-light); | |
| text-decoration: underline; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Header --> | |
| <div class="header"> | |
| <div class="header-left"> | |
| <div class="logo">Open Voice Agent</div> | |
| <div class="status-badge"> | |
| <div class="status-dot" id="status-dot"></div> | |
| <span id="status">Idle</span> | |
| </div> | |
| </div> | |
| <div class="controls"> | |
| <button id="connect" class="btn-primary"> | |
| <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg> | |
| Connect | |
| </button> | |
| <button id="disconnect" class="btn-danger" disabled> | |
| <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/></svg> | |
| Disconnect | |
| </button> | |
| <button id="mute" disabled> | |
| <svg id="mic-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" x2="12" y1="19" y2="22"/></svg> | |
| Mute | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Main single-column layout --> | |
| <div class="main"> | |
| <!-- Waveform hero --> | |
| <div class="waveform-section"> | |
| <canvas id="wave" width="900" height="200"></canvas> | |
| </div> | |
| <!-- Live metric cards --> | |
| <div class="live-metrics-row"> | |
| <div class="pipeline-header"> | |
| <span class="pipeline-title">Voice Agent Pipeline</span> | |
| <span class="pipeline-subtitle">Core stages from end-of-speech to first assistant audio</span> | |
| </div> | |
| <div class="pipeline-stage-row" id="pipeline-stage-row"> | |
| <div class="metric-card stage-card"> | |
| <div class="metric-card-top"> | |
| <span class="metric-step">1</span> | |
| <div class="metric-tech-wrap"> | |
| <span class="metric-tech">EOU Delay</span> | |
| <div class="tooltip"> | |
| <button type="button" class="tooltip-trigger" aria-label="What is EOU Delay?">i</button> | |
| <span class="tooltip-content" role="tooltip">Time from your last speech frame until the turn is considered complete. In this pipeline it already includes transcription wait.</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="metric-card-human">Silence Detection</div> | |
| <div class="metric-card-desc">Deciding that your turn has ended</div> | |
| <div class="metric-card-value-row"> | |
| <span class="metric-card-value" id="live-eou">--</span> | |
| <span class="metric-card-avg" id="live-eou-avg"></span> | |
| </div> | |
| <div class="metric-card-track"> | |
| <div class="metric-card-fill" id="live-eou-bar" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| <div class="metric-card stage-card"> | |
| <div class="metric-card-top"> | |
| <span class="metric-step">2</span> | |
| <div class="metric-tech-wrap"> | |
| <span class="metric-tech">LLM TTFT</span> | |
| <div class="tooltip"> | |
| <button type="button" class="tooltip-trigger" aria-label="What is LLM TTFT?">i</button> | |
| <span class="tooltip-content" role="tooltip">Thinking time: from LLM request start to the first generated token.</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="metric-card-human">Thinking</div> | |
| <div class="metric-card-desc">Generating the first token</div> | |
| <div class="metric-card-value-row"> | |
| <span class="metric-card-value" id="live-llm-ttft">--</span> | |
| <span class="metric-card-avg" id="live-llm-ttft-avg"></span> | |
| </div> | |
| <div class="metric-card-track"> | |
| <div class="metric-card-fill" id="live-llm-ttft-bar" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| <div class="metric-card stage-card" id="live-handoff-card" hidden> | |
| <div class="metric-card-top"> | |
| <span class="metric-step">3</span> | |
| <div class="metric-tech-wrap"> | |
| <span class="metric-tech">LLM to TTS Handoff</span> | |
| <div class="tooltip"> | |
| <button type="button" class="tooltip-trigger" aria-label="What is LLM to TTS Handoff?">i</button> | |
| <span class="tooltip-content" role="tooltip">Residual orchestration gap between LLM output and TTS startup. It is shown only when this gap is measurable and greater than zero.</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="metric-card-human">Pipeline Handoff</div> | |
| <div class="metric-card-desc">Bridging text generation to speech synthesis</div> | |
| <div class="metric-card-value-row"> | |
| <span class="metric-card-value" id="live-handoff">--</span> | |
| <span class="metric-card-avg" id="live-handoff-avg"></span> | |
| </div> | |
| <div class="metric-card-track"> | |
| <div class="metric-card-fill" id="live-handoff-bar" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| <div class="metric-card stage-card"> | |
| <div class="metric-card-top"> | |
| <span class="metric-step" id="live-voice-generation-step">3</span> | |
| <div class="metric-tech-wrap"> | |
| <span class="metric-tech">TTS TTFB</span> | |
| <div class="tooltip"> | |
| <button type="button" class="tooltip-trigger" aria-label="What is Voice Generation?">i</button> | |
| <span class="tooltip-content" role="tooltip">Voice generation startup only: time from TTS request start to the first audio chunk (TTFB).</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="metric-card-human">Voice Generation</div> | |
| <div class="metric-card-desc">Starting audio synthesis</div> | |
| <div class="metric-card-value-row"> | |
| <span class="metric-card-value" id="live-voice-generation">--</span> | |
| <span class="metric-card-avg" id="live-voice-generation-avg"></span> | |
| </div> | |
| <div class="metric-card-track"> | |
| <div class="metric-card-fill" id="live-voice-generation-bar" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="pipeline-total-card"> | |
| <div class="pipeline-total-top"> | |
| <div> | |
| <div class="pipeline-total-label">Total Round-Trip</div> | |
| <div class="pipeline-total-tech"> | |
| <span>End-to-End Latency</span> | |
| <div class="tooltip tooltip-right"> | |
| <button type="button" class="tooltip-trigger" aria-label="What is End-to-End Latency?">i</button> | |
| <span class="tooltip-content" role="tooltip">Total round-trip from end of user speech to first assistant audio: EOU delay + Thinking (LLM TTFT) + optional handoff + Voice Generation (TTS TTFB).</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="pipeline-total-value-wrap"> | |
| <span class="metric-card-value pipeline-total-value" id="live-total">--</span> | |
| <span class="pipeline-total-avg" id="live-total-avg"></span> | |
| </div> | |
| </div> | |
| <div class="metric-card-track"> | |
| <div class="metric-card-fill" id="live-total-bar" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="footer-note"> | |
| {{FOOTER_POWERED_BY}} | |
| </div> | |
| </div> | |
| <audio id="remote-audio" autoplay></audio> | |
| <script src="https://unpkg.com/livekit-client/dist/livekit-client.umd.js"></script> | |
| <script> | |
| </script> | |
| <script> | |
| </script> | |
| </body> | |
| </html> | |