biteve / frontend /public /livekit.html
AIBRUH's picture
Upload frontend/public/livekit.html with huggingface_hub
bac35d4 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EVE β€” Live</title>
<script src="https://cdn.jsdelivr.net/npm/livekit-client@2/dist/livekit-client.umd.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #0a0a0f;
color: #e0e0e0;
font-family: system-ui, sans-serif;
height: 100vh;
display: grid;
grid-template-columns: 1fr 380px;
grid-template-rows: 1fr;
overflow: hidden;
}
/* Left: Eve video */
.video-panel {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #0a0a0f;
position: relative;
}
#video-container {
width: 100%;
max-width: 600px;
aspect-ratio: 1;
border-radius: 12px;
overflow: hidden;
border: 2px solid rgba(167,139,250,0.2);
box-shadow: 0 0 80px rgba(167,139,250,0.1);
background: #111;
display: flex;
align-items: center;
justify-content: center;
}
#video-container video {
width: 100%;
height: 100%;
object-fit: cover;
}
.video-status {
position: absolute;
top: 16px;
left: 16px;
display: flex;
align-items: center;
gap: 8px;
font-size: 0.75rem;
color: #666;
background: rgba(0,0,0,0.6);
padding: 6px 12px;
border-radius: 20px;
}
.dot { width: 8px; height: 8px; border-radius: 50%; background: #666; }
.dot.live { background: #22c55e; animation: blink 2s infinite; }
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.4} }
.mic-controls {
position: absolute;
bottom: 20px;
display: flex;
gap: 12px;
align-items: center;
}
.mic-btn {
width: 56px; height: 56px;
border-radius: 50%;
border: none;
background: #4c1d95;
color: white;
font-size: 1.4rem;
cursor: pointer;
transition: all 0.2s;
}
.mic-btn:hover { background: #5b21b6; transform: scale(1.05); }
.mic-btn.active { background: #dc2626; animation: pulse 1.5s infinite; }
@keyframes pulse {
0%,100% { box-shadow: 0 0 0 0 rgba(220,38,38,0.4); }
50% { box-shadow: 0 0 0 14px rgba(220,38,38,0); }
}
.mic-label { font-size: 0.75rem; color: #888; }
/* Right: Chat panel */
.chat-panel {
display: flex;
flex-direction: column;
background: #111118;
border-left: 1px solid #222;
}
.chat-header {
padding: 16px 20px;
border-bottom: 1px solid #222;
text-align: center;
}
.chat-header h2 {
font-size: 1.3rem;
font-weight: 200;
letter-spacing: 0.3em;
color: #a78bfa;
}
.chat-header p {
font-size: 0.7rem;
color: #555;
margin-top: 4px;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 10px;
}
.chat-messages::-webkit-scrollbar { width: 4px; }
.chat-messages::-webkit-scrollbar-thumb { background: #333; border-radius: 2px; }
.msg {
padding: 10px 14px;
border-radius: 12px;
max-width: 90%;
font-size: 0.85rem;
line-height: 1.5;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:translateY(0)} }
.msg.eve {
background: linear-gradient(135deg, #1e1b4b, #2a1a4e);
align-self: flex-start;
border: 1px solid #312e81;
color: #c4b5fd;
}
.msg.eve .sender { color: #a78bfa; font-size: 0.7rem; font-weight: 600; margin-bottom: 4px; }
.msg.user {
background: #1f2937;
align-self: flex-end;
border: 1px solid #374151;
color: #d1d5db;
}
.msg.user .sender { color: #9ca3af; font-size: 0.7rem; font-weight: 600; margin-bottom: 4px; text-align: right; }
.msg.system {
background: transparent;
align-self: center;
color: #555;
font-size: 0.75rem;
padding: 4px;
}
.msg.thinking {
background: linear-gradient(135deg, #1e1b4b, #2a1a4e);
align-self: flex-start;
border: 1px solid #312e81;
color: #7c6fc4;
font-style: italic;
}
.chat-input {
display: flex;
gap: 8px;
padding: 12px 16px;
border-top: 1px solid #222;
background: #0d0d14;
}
.chat-input input {
flex: 1;
background: #1a1a24;
border: 1px solid #333;
border-radius: 8px;
padding: 10px 14px;
color: #e0e0e0;
font-size: 0.85rem;
outline: none;
}
.chat-input input:focus { border-color: #a78bfa; }
.chat-input button {
background: #4c1d95;
color: white;
border: none;
border-radius: 8px;
padding: 10px 18px;
cursor: pointer;
font-size: 0.85rem;
transition: background 0.2s;
}
.chat-input button:hover { background: #5b21b6; }
.chat-input button:disabled { opacity: 0.4; cursor: not-allowed; }
</style>
</head>
<body>
<!-- Left: Eve video -->
<div class="video-panel">
<div class="video-status">
<span class="dot" id="live-dot"></span>
<span id="status-text">Connecting...</span>
</div>
<div id="video-container">
<span style="color:#444">Waiting for stream...</span>
</div>
<div class="mic-controls">
<button class="mic-btn" id="mic-btn" onclick="toggleMic()">🎀</button>
<span class="mic-label" id="mic-label">Mic off</span>
</div>
</div>
<!-- Right: Chat -->
<div class="chat-panel">
<div class="chat-header">
<h2>E V E</h2>
<p>bitHuman Neural Avatar | LiveKit WebRTC</p>
</div>
<div class="chat-messages" id="messages">
</div>
<div class="chat-input">
<input type="text" id="chat-input" placeholder="Talk to Eve..."
onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendText()}">
<button id="send-btn" onclick="sendText()">Send</button>
</div>
</div>
<script>
const LIVEKIT_URL = 'wss://tall-cotton-nvhnfg10.livekit.cloud';
const GATEWAY_URL = 'http://localhost:8000';
let room = null;
let micEnabled = false;
let thinkingDiv = null;
// ── Chat ────────────────────────────────────────────────
function addMessage(role, text) {
const container = document.getElementById('messages');
const div = document.createElement('div');
div.className = `msg ${role}`;
const sender = document.createElement('div');
sender.className = 'sender';
sender.textContent = role === 'eve' ? 'Eve' : role === 'user' ? 'You' : '';
if (role !== 'system' && role !== 'thinking') div.appendChild(sender);
const body = document.createElement('div');
body.textContent = text;
div.appendChild(body);
container.appendChild(div);
container.scrollTop = container.scrollHeight;
return div;
}
async function sendText() {
const input = document.getElementById('chat-input');
const text = input.value.trim();
if (!text || !room) return;
input.value = '';
addMessage('user', text);
// Show thinking indicator
thinkingDiv = addMessage('thinking', 'Eve is thinking...');
// Send via LiveKit data channel β€” GPU agent handles Grok + TTS + lip sync
const payload = JSON.stringify({ type: 'chat', text: text });
const encoder = new TextEncoder();
room.localParticipant.publishData(encoder.encode(payload), { reliable: true });
}
// ── LiveKit ─────────────────────────────────────────────
async function getToken() {
const resp = await fetch(`${GATEWAY_URL}/livekit-token`);
return (await resp.json()).token;
}
async function connectToEve() {
const statusText = document.getElementById('status-text');
const liveDot = document.getElementById('live-dot');
const container = document.getElementById('video-container');
try {
const token = await getToken();
room = new LivekitClient.Room({
adaptiveStream: true,
dynacast: true,
audioCaptureDefaults: {
autoGainControl: true,
echoCancellation: true,
noiseSuppression: true,
},
});
room.on(LivekitClient.RoomEvent.TrackSubscribed, (track, pub, participant) => {
// Only subscribe to remote participants (Eve), never our own tracks
if (participant.identity === room.localParticipant.identity) return;
if (track.kind === 'video') {
const el = track.attach();
el.style.width = '100%';
el.style.height = '100%';
el.style.objectFit = 'cover';
container.innerHTML = '';
container.appendChild(el);
}
if (track.kind === 'audio') {
const el = track.attach();
el.style.display = 'none';
document.body.appendChild(el);
}
});
room.on(LivekitClient.RoomEvent.TrackUnsubscribed, (track) => {
track.detach().forEach(el => el.remove());
});
// Receive Eve's text responses via data channel
room.on(LivekitClient.RoomEvent.DataReceived, (data, participant) => {
try {
const decoder = new TextDecoder();
const msg = JSON.parse(decoder.decode(data));
if (msg.type === 'eve_response' && msg.text) {
// Remove thinking indicator
if (thinkingDiv) {
thinkingDiv.remove();
thinkingDiv = null;
}
addMessage('eve', msg.text);
}
} catch (e) {
console.error('Data channel parse error:', e);
}
});
room.on(LivekitClient.RoomEvent.Connected, async () => {
statusText.textContent = 'Eve is live';
liveDot.classList.add('live');
// Mic starts OFF β€” user clicks mic button to enable
document.getElementById('mic-label').textContent = 'Click to talk';
});
room.on(LivekitClient.RoomEvent.Disconnected, () => {
statusText.textContent = 'Disconnected';
liveDot.classList.remove('live');
addMessage('system', 'Disconnected');
});
await room.connect(LIVEKIT_URL, token);
} catch (err) {
statusText.textContent = `Error: ${err.message}`;
addMessage('system', `Connection error: ${err.message}`);
}
}
async function enableMic() {
if (!room) return;
try {
await room.localParticipant.setMicrophoneEnabled(true);
micEnabled = true;
document.getElementById('mic-btn').classList.add('active');
document.getElementById('mic-label').textContent = 'Mic live';
} catch (err) {
document.getElementById('mic-label').textContent = 'Mic denied';
}
}
async function toggleMic() {
if (!room) return;
if (micEnabled) {
await room.localParticipant.setMicrophoneEnabled(false);
micEnabled = false;
document.getElementById('mic-btn').classList.remove('active');
document.getElementById('mic-label').textContent = 'Mic off';
} else {
await enableMic();
}
}
// Auto-connect
window.addEventListener('load', () => setTimeout(connectToEve, 300));
</script>
</body>
</html>