AIBRUH commited on
Commit
bac35d4
Β·
verified Β·
1 Parent(s): afd71cc

Upload frontend/public/livekit.html with huggingface_hub

Browse files
Files changed (1) hide show
  1. frontend/public/livekit.html +385 -0
frontend/public/livekit.html ADDED
@@ -0,0 +1,385 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>EVE β€” Live</title>
7
+ <script src="https://cdn.jsdelivr.net/npm/livekit-client@2/dist/livekit-client.umd.js"></script>
8
+ <style>
9
+ * { margin: 0; padding: 0; box-sizing: border-box; }
10
+ body {
11
+ background: #0a0a0f;
12
+ color: #e0e0e0;
13
+ font-family: system-ui, sans-serif;
14
+ height: 100vh;
15
+ display: grid;
16
+ grid-template-columns: 1fr 380px;
17
+ grid-template-rows: 1fr;
18
+ overflow: hidden;
19
+ }
20
+
21
+ /* Left: Eve video */
22
+ .video-panel {
23
+ display: flex;
24
+ flex-direction: column;
25
+ align-items: center;
26
+ justify-content: center;
27
+ background: #0a0a0f;
28
+ position: relative;
29
+ }
30
+ #video-container {
31
+ width: 100%;
32
+ max-width: 600px;
33
+ aspect-ratio: 1;
34
+ border-radius: 12px;
35
+ overflow: hidden;
36
+ border: 2px solid rgba(167,139,250,0.2);
37
+ box-shadow: 0 0 80px rgba(167,139,250,0.1);
38
+ background: #111;
39
+ display: flex;
40
+ align-items: center;
41
+ justify-content: center;
42
+ }
43
+ #video-container video {
44
+ width: 100%;
45
+ height: 100%;
46
+ object-fit: cover;
47
+ }
48
+ .video-status {
49
+ position: absolute;
50
+ top: 16px;
51
+ left: 16px;
52
+ display: flex;
53
+ align-items: center;
54
+ gap: 8px;
55
+ font-size: 0.75rem;
56
+ color: #666;
57
+ background: rgba(0,0,0,0.6);
58
+ padding: 6px 12px;
59
+ border-radius: 20px;
60
+ }
61
+ .dot { width: 8px; height: 8px; border-radius: 50%; background: #666; }
62
+ .dot.live { background: #22c55e; animation: blink 2s infinite; }
63
+ @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.4} }
64
+
65
+ .mic-controls {
66
+ position: absolute;
67
+ bottom: 20px;
68
+ display: flex;
69
+ gap: 12px;
70
+ align-items: center;
71
+ }
72
+ .mic-btn {
73
+ width: 56px; height: 56px;
74
+ border-radius: 50%;
75
+ border: none;
76
+ background: #4c1d95;
77
+ color: white;
78
+ font-size: 1.4rem;
79
+ cursor: pointer;
80
+ transition: all 0.2s;
81
+ }
82
+ .mic-btn:hover { background: #5b21b6; transform: scale(1.05); }
83
+ .mic-btn.active { background: #dc2626; animation: pulse 1.5s infinite; }
84
+ @keyframes pulse {
85
+ 0%,100% { box-shadow: 0 0 0 0 rgba(220,38,38,0.4); }
86
+ 50% { box-shadow: 0 0 0 14px rgba(220,38,38,0); }
87
+ }
88
+ .mic-label { font-size: 0.75rem; color: #888; }
89
+
90
+ /* Right: Chat panel */
91
+ .chat-panel {
92
+ display: flex;
93
+ flex-direction: column;
94
+ background: #111118;
95
+ border-left: 1px solid #222;
96
+ }
97
+ .chat-header {
98
+ padding: 16px 20px;
99
+ border-bottom: 1px solid #222;
100
+ text-align: center;
101
+ }
102
+ .chat-header h2 {
103
+ font-size: 1.3rem;
104
+ font-weight: 200;
105
+ letter-spacing: 0.3em;
106
+ color: #a78bfa;
107
+ }
108
+ .chat-header p {
109
+ font-size: 0.7rem;
110
+ color: #555;
111
+ margin-top: 4px;
112
+ }
113
+
114
+ .chat-messages {
115
+ flex: 1;
116
+ overflow-y: auto;
117
+ padding: 16px;
118
+ display: flex;
119
+ flex-direction: column;
120
+ gap: 10px;
121
+ }
122
+ .chat-messages::-webkit-scrollbar { width: 4px; }
123
+ .chat-messages::-webkit-scrollbar-thumb { background: #333; border-radius: 2px; }
124
+
125
+ .msg {
126
+ padding: 10px 14px;
127
+ border-radius: 12px;
128
+ max-width: 90%;
129
+ font-size: 0.85rem;
130
+ line-height: 1.5;
131
+ animation: fadeIn 0.3s ease;
132
+ }
133
+ @keyframes fadeIn { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:translateY(0)} }
134
+
135
+ .msg.eve {
136
+ background: linear-gradient(135deg, #1e1b4b, #2a1a4e);
137
+ align-self: flex-start;
138
+ border: 1px solid #312e81;
139
+ color: #c4b5fd;
140
+ }
141
+ .msg.eve .sender { color: #a78bfa; font-size: 0.7rem; font-weight: 600; margin-bottom: 4px; }
142
+ .msg.user {
143
+ background: #1f2937;
144
+ align-self: flex-end;
145
+ border: 1px solid #374151;
146
+ color: #d1d5db;
147
+ }
148
+ .msg.user .sender { color: #9ca3af; font-size: 0.7rem; font-weight: 600; margin-bottom: 4px; text-align: right; }
149
+ .msg.system {
150
+ background: transparent;
151
+ align-self: center;
152
+ color: #555;
153
+ font-size: 0.75rem;
154
+ padding: 4px;
155
+ }
156
+ .msg.thinking {
157
+ background: linear-gradient(135deg, #1e1b4b, #2a1a4e);
158
+ align-self: flex-start;
159
+ border: 1px solid #312e81;
160
+ color: #7c6fc4;
161
+ font-style: italic;
162
+ }
163
+
164
+ .chat-input {
165
+ display: flex;
166
+ gap: 8px;
167
+ padding: 12px 16px;
168
+ border-top: 1px solid #222;
169
+ background: #0d0d14;
170
+ }
171
+ .chat-input input {
172
+ flex: 1;
173
+ background: #1a1a24;
174
+ border: 1px solid #333;
175
+ border-radius: 8px;
176
+ padding: 10px 14px;
177
+ color: #e0e0e0;
178
+ font-size: 0.85rem;
179
+ outline: none;
180
+ }
181
+ .chat-input input:focus { border-color: #a78bfa; }
182
+ .chat-input button {
183
+ background: #4c1d95;
184
+ color: white;
185
+ border: none;
186
+ border-radius: 8px;
187
+ padding: 10px 18px;
188
+ cursor: pointer;
189
+ font-size: 0.85rem;
190
+ transition: background 0.2s;
191
+ }
192
+ .chat-input button:hover { background: #5b21b6; }
193
+ .chat-input button:disabled { opacity: 0.4; cursor: not-allowed; }
194
+ </style>
195
+ </head>
196
+ <body>
197
+
198
+ <!-- Left: Eve video -->
199
+ <div class="video-panel">
200
+ <div class="video-status">
201
+ <span class="dot" id="live-dot"></span>
202
+ <span id="status-text">Connecting...</span>
203
+ </div>
204
+ <div id="video-container">
205
+ <span style="color:#444">Waiting for stream...</span>
206
+ </div>
207
+ <div class="mic-controls">
208
+ <button class="mic-btn" id="mic-btn" onclick="toggleMic()">🎀</button>
209
+ <span class="mic-label" id="mic-label">Mic off</span>
210
+ </div>
211
+ </div>
212
+
213
+ <!-- Right: Chat -->
214
+ <div class="chat-panel">
215
+ <div class="chat-header">
216
+ <h2>E V E</h2>
217
+ <p>bitHuman Neural Avatar | LiveKit WebRTC</p>
218
+ </div>
219
+ <div class="chat-messages" id="messages">
220
+ </div>
221
+ <div class="chat-input">
222
+ <input type="text" id="chat-input" placeholder="Talk to Eve..."
223
+ onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendText()}">
224
+ <button id="send-btn" onclick="sendText()">Send</button>
225
+ </div>
226
+ </div>
227
+
228
+ <script>
229
+ const LIVEKIT_URL = 'wss://tall-cotton-nvhnfg10.livekit.cloud';
230
+ const GATEWAY_URL = 'http://localhost:8000';
231
+ let room = null;
232
+ let micEnabled = false;
233
+ let thinkingDiv = null;
234
+
235
+ // ── Chat ────────────────────────────────────────────────
236
+ function addMessage(role, text) {
237
+ const container = document.getElementById('messages');
238
+ const div = document.createElement('div');
239
+ div.className = `msg ${role}`;
240
+
241
+ const sender = document.createElement('div');
242
+ sender.className = 'sender';
243
+ sender.textContent = role === 'eve' ? 'Eve' : role === 'user' ? 'You' : '';
244
+
245
+ if (role !== 'system' && role !== 'thinking') div.appendChild(sender);
246
+ const body = document.createElement('div');
247
+ body.textContent = text;
248
+ div.appendChild(body);
249
+
250
+ container.appendChild(div);
251
+ container.scrollTop = container.scrollHeight;
252
+ return div;
253
+ }
254
+
255
+ async function sendText() {
256
+ const input = document.getElementById('chat-input');
257
+ const text = input.value.trim();
258
+ if (!text || !room) return;
259
+
260
+ input.value = '';
261
+ addMessage('user', text);
262
+
263
+ // Show thinking indicator
264
+ thinkingDiv = addMessage('thinking', 'Eve is thinking...');
265
+
266
+ // Send via LiveKit data channel β€” GPU agent handles Grok + TTS + lip sync
267
+ const payload = JSON.stringify({ type: 'chat', text: text });
268
+ const encoder = new TextEncoder();
269
+ room.localParticipant.publishData(encoder.encode(payload), { reliable: true });
270
+ }
271
+
272
+ // ── LiveKit ─────────────────────────────────────────────
273
+ async function getToken() {
274
+ const resp = await fetch(`${GATEWAY_URL}/livekit-token`);
275
+ return (await resp.json()).token;
276
+ }
277
+
278
+ async function connectToEve() {
279
+ const statusText = document.getElementById('status-text');
280
+ const liveDot = document.getElementById('live-dot');
281
+ const container = document.getElementById('video-container');
282
+
283
+ try {
284
+ const token = await getToken();
285
+
286
+ room = new LivekitClient.Room({
287
+ adaptiveStream: true,
288
+ dynacast: true,
289
+ audioCaptureDefaults: {
290
+ autoGainControl: true,
291
+ echoCancellation: true,
292
+ noiseSuppression: true,
293
+ },
294
+ });
295
+
296
+ room.on(LivekitClient.RoomEvent.TrackSubscribed, (track, pub, participant) => {
297
+ // Only subscribe to remote participants (Eve), never our own tracks
298
+ if (participant.identity === room.localParticipant.identity) return;
299
+ if (track.kind === 'video') {
300
+ const el = track.attach();
301
+ el.style.width = '100%';
302
+ el.style.height = '100%';
303
+ el.style.objectFit = 'cover';
304
+ container.innerHTML = '';
305
+ container.appendChild(el);
306
+ }
307
+ if (track.kind === 'audio') {
308
+ const el = track.attach();
309
+ el.style.display = 'none';
310
+ document.body.appendChild(el);
311
+ }
312
+ });
313
+
314
+ room.on(LivekitClient.RoomEvent.TrackUnsubscribed, (track) => {
315
+ track.detach().forEach(el => el.remove());
316
+ });
317
+
318
+ // Receive Eve's text responses via data channel
319
+ room.on(LivekitClient.RoomEvent.DataReceived, (data, participant) => {
320
+ try {
321
+ const decoder = new TextDecoder();
322
+ const msg = JSON.parse(decoder.decode(data));
323
+ if (msg.type === 'eve_response' && msg.text) {
324
+ // Remove thinking indicator
325
+ if (thinkingDiv) {
326
+ thinkingDiv.remove();
327
+ thinkingDiv = null;
328
+ }
329
+ addMessage('eve', msg.text);
330
+ }
331
+ } catch (e) {
332
+ console.error('Data channel parse error:', e);
333
+ }
334
+ });
335
+
336
+ room.on(LivekitClient.RoomEvent.Connected, async () => {
337
+ statusText.textContent = 'Eve is live';
338
+ liveDot.classList.add('live');
339
+ // Mic starts OFF β€” user clicks mic button to enable
340
+ document.getElementById('mic-label').textContent = 'Click to talk';
341
+ });
342
+
343
+ room.on(LivekitClient.RoomEvent.Disconnected, () => {
344
+ statusText.textContent = 'Disconnected';
345
+ liveDot.classList.remove('live');
346
+ addMessage('system', 'Disconnected');
347
+ });
348
+
349
+ await room.connect(LIVEKIT_URL, token);
350
+
351
+ } catch (err) {
352
+ statusText.textContent = `Error: ${err.message}`;
353
+ addMessage('system', `Connection error: ${err.message}`);
354
+ }
355
+ }
356
+
357
+ async function enableMic() {
358
+ if (!room) return;
359
+ try {
360
+ await room.localParticipant.setMicrophoneEnabled(true);
361
+ micEnabled = true;
362
+ document.getElementById('mic-btn').classList.add('active');
363
+ document.getElementById('mic-label').textContent = 'Mic live';
364
+ } catch (err) {
365
+ document.getElementById('mic-label').textContent = 'Mic denied';
366
+ }
367
+ }
368
+
369
+ async function toggleMic() {
370
+ if (!room) return;
371
+ if (micEnabled) {
372
+ await room.localParticipant.setMicrophoneEnabled(false);
373
+ micEnabled = false;
374
+ document.getElementById('mic-btn').classList.remove('active');
375
+ document.getElementById('mic-label').textContent = 'Mic off';
376
+ } else {
377
+ await enableMic();
378
+ }
379
+ }
380
+
381
+ // Auto-connect
382
+ window.addEventListener('load', () => setTimeout(connectToEve, 300));
383
+ </script>
384
+ </body>
385
+ </html>