on1onmangoes commited on
Commit
7a929fb
·
verified ·
1 Parent(s): 4e0f4ac

Upload index.html with huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +429 -0
index.html ADDED
@@ -0,0 +1,429 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Real-time Whisper Transcription</title>
8
+ <style>
9
+ :root {
10
+ --primary-gradient: linear-gradient(135deg, #f9a45c 0%, #e66465 100%);
11
+ --background-cream: #faf8f5;
12
+ --text-dark: #2d2d2d;
13
+ }
14
+
15
+ body {
16
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
17
+ margin: 0;
18
+ padding: 0;
19
+ background-color: var(--background-cream);
20
+ color: var(--text-dark);
21
+ min-height: 100vh;
22
+ }
23
+
24
+ .hero {
25
+ background: var(--primary-gradient);
26
+ color: white;
27
+ padding: 2.5rem 2rem;
28
+ text-align: center;
29
+ }
30
+
31
+ .hero h1 {
32
+ font-size: 2.5rem;
33
+ margin: 0;
34
+ font-weight: 600;
35
+ letter-spacing: -0.5px;
36
+ }
37
+
38
+ .hero p {
39
+ font-size: 1rem;
40
+ margin-top: 0.5rem;
41
+ opacity: 0.9;
42
+ }
43
+
44
+ .container {
45
+ max-width: 1000px;
46
+ margin: 1.5rem auto;
47
+ padding: 0 2rem;
48
+ }
49
+
50
+ .transcript-container {
51
+ border-radius: 8px;
52
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
53
+ padding: 1.5rem;
54
+ height: 300px;
55
+ overflow-y: auto;
56
+ margin-bottom: 1.5rem;
57
+ border: 1px solid rgba(0, 0, 0, 0.1);
58
+ }
59
+
60
+ .controls {
61
+ text-align: center;
62
+ margin: 1.5rem 0;
63
+ }
64
+
65
+ button {
66
+ background: var(--primary-gradient);
67
+ color: white;
68
+ border: none;
69
+ padding: 10px 20px;
70
+ font-size: 0.95rem;
71
+ border-radius: 6px;
72
+ cursor: pointer;
73
+ transition: all 0.2s ease;
74
+ font-weight: 500;
75
+ min-width: 180px;
76
+ }
77
+
78
+ button:hover {
79
+ transform: translateY(-1px);
80
+ box-shadow: 0 4px 12px rgba(230, 100, 101, 0.15);
81
+ }
82
+
83
+ button:active {
84
+ transform: translateY(0);
85
+ }
86
+
87
+ /* Transcript text styling */
88
+ .transcript-container p {
89
+ margin: 0.4rem 0;
90
+ padding: 0.6rem;
91
+ background: var(--background-cream);
92
+ border-radius: 4px;
93
+ line-height: 1.4;
94
+ font-size: 0.95rem;
95
+ }
96
+
97
+ /* Custom scrollbar - made thinner */
98
+ .transcript-container::-webkit-scrollbar {
99
+ width: 6px;
100
+ }
101
+
102
+ .transcript-container::-webkit-scrollbar-track {
103
+ background: var(--background-cream);
104
+ border-radius: 3px;
105
+ }
106
+
107
+ .transcript-container::-webkit-scrollbar-thumb {
108
+ background: #e66465;
109
+ border-radius: 3px;
110
+ opacity: 0.8;
111
+ }
112
+
113
+ .transcript-container::-webkit-scrollbar-thumb:hover {
114
+ background: #f9a45c;
115
+ }
116
+
117
+ /* Add styles for toast notifications */
118
+ .toast {
119
+ position: fixed;
120
+ top: 20px;
121
+ left: 50%;
122
+ transform: translateX(-50%);
123
+ padding: 16px 24px;
124
+ border-radius: 4px;
125
+ font-size: 14px;
126
+ z-index: 1000;
127
+ display: none;
128
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
129
+ }
130
+
131
+ .toast.error {
132
+ background-color: #f44336;
133
+ color: white;
134
+ }
135
+
136
+ .toast.warning {
137
+ background-color: #ffd700;
138
+ color: black;
139
+ }
140
+
141
+ /* Add styles for audio visualization */
142
+ .icon-with-spinner {
143
+ display: flex;
144
+ align-items: center;
145
+ justify-content: center;
146
+ gap: 12px;
147
+ min-width: 180px;
148
+ }
149
+
150
+ .spinner {
151
+ width: 20px;
152
+ height: 20px;
153
+ border: 2px solid white;
154
+ border-top-color: transparent;
155
+ border-radius: 50%;
156
+ animation: spin 1s linear infinite;
157
+ flex-shrink: 0;
158
+ }
159
+
160
+ .pulse-container {
161
+ display: flex;
162
+ align-items: center;
163
+ justify-content: center;
164
+ gap: 12px;
165
+ min-width: 180px;
166
+ }
167
+
168
+ .pulse-circle {
169
+ width: 20px;
170
+ height: 20px;
171
+ border-radius: 50%;
172
+ background-color: white;
173
+ opacity: 0.2;
174
+ flex-shrink: 0;
175
+ transform: translateX(-0%) scale(var(--audio-level, 1));
176
+ transition: transform 0.1s ease;
177
+ }
178
+
179
+ @keyframes spin {
180
+ to {
181
+ transform: rotate(360deg);
182
+ }
183
+ }
184
+ </style>
185
+ </head>
186
+
187
+ <body>
188
+ <!-- Add toast element after body opening tag -->
189
+ <div id="error-toast" class="toast"></div>
190
+ <div class="hero">
191
+ <h1>Real-time Transcription</h1>
192
+ <p>Powered by FastRTC and Local Whisper 🤗</p>
193
+ </div>
194
+
195
+ <div class="container">
196
+ <div class="transcript-container" id="transcript"></div>
197
+ <div class="controls">
198
+ <button id="start-button">Start Recording</button>
199
+ </div>
200
+ </div>
201
+
202
+ <script>
203
+ let peerConnection;
204
+ let webrtc_id;
205
+ let audioContext, analyser, audioSource;
206
+ let audioLevel = 0;
207
+ let animationFrame;
208
+ let eventSource;
209
+
210
+ const startButton = document.getElementById('start-button');
211
+ const transcriptDiv = document.getElementById('transcript');
212
+
213
+ function showError(message) {
214
+ const toast = document.getElementById('error-toast');
215
+ toast.textContent = message;
216
+ toast.style.display = 'block';
217
+
218
+ // Hide toast after 5 seconds
219
+ setTimeout(() => {
220
+ toast.style.display = 'none';
221
+ }, 5000);
222
+ }
223
+
224
+ function handleMessage(event) {
225
+ // Handle any WebRTC data channel messages if needed
226
+ const eventJson = JSON.parse(event.data);
227
+ if (eventJson.type === "error") {
228
+ showError(eventJson.message);
229
+ }
230
+ console.log('Received message:', event.data);
231
+ }
232
+
233
+ function updateButtonState() {
234
+ if (peerConnection && (peerConnection.connectionState === 'connecting' || peerConnection.connectionState === 'new')) {
235
+ startButton.innerHTML = `
236
+ <div class="icon-with-spinner">
237
+ <div class="spinner"></div>
238
+ <span>Connecting...</span>
239
+ </div>
240
+ `;
241
+ } else if (peerConnection && peerConnection.connectionState === 'connected') {
242
+ startButton.innerHTML = `
243
+ <div class="pulse-container">
244
+ <div class="pulse-circle"></div>
245
+ <span>Stop Recording</span>
246
+ </div>
247
+ `;
248
+ } else {
249
+ startButton.innerHTML = 'Start Recording';
250
+ }
251
+ }
252
+
253
+ function setupAudioVisualization(stream) {
254
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
255
+ analyser = audioContext.createAnalyser();
256
+ audioSource = audioContext.createMediaStreamSource(stream);
257
+ audioSource.connect(analyser);
258
+ analyser.fftSize = 64;
259
+ const dataArray = new Uint8Array(analyser.frequencyBinCount);
260
+
261
+ function updateAudioLevel() {
262
+ analyser.getByteFrequencyData(dataArray);
263
+ const average = Array.from(dataArray).reduce((a, b) => a + b, 0) / dataArray.length;
264
+ audioLevel = average / 255;
265
+
266
+ const pulseCircle = document.querySelector('.pulse-circle');
267
+ if (pulseCircle) {
268
+ pulseCircle.style.setProperty('--audio-level', 1 + audioLevel);
269
+ }
270
+
271
+ animationFrame = requestAnimationFrame(updateAudioLevel);
272
+ }
273
+ updateAudioLevel();
274
+ }
275
+
276
+ async function setupWebRTC() {
277
+ const config = __RTC_CONFIGURATION__;
278
+ peerConnection = new RTCPeerConnection(config);
279
+
280
+ const timeoutId = setTimeout(() => {
281
+ const toast = document.getElementById('error-toast');
282
+ toast.textContent = "Connection is taking longer than usual. Are you on a VPN?";
283
+ toast.className = 'toast warning';
284
+ toast.style.display = 'block';
285
+
286
+ // Hide warning after 5 seconds
287
+ setTimeout(() => {
288
+ toast.style.display = 'none';
289
+ }, 5000);
290
+ }, 5000);
291
+
292
+ try {
293
+ const stream = await navigator.mediaDevices.getUserMedia({
294
+ audio: true
295
+ });
296
+
297
+ setupAudioVisualization(stream);
298
+
299
+ stream.getTracks().forEach(track => {
300
+ peerConnection.addTrack(track, stream);
301
+ });
302
+
303
+ // Add connection state change listener
304
+ peerConnection.addEventListener('connectionstatechange', () => {
305
+ console.log('connectionstatechange', peerConnection.connectionState);
306
+ if (peerConnection.connectionState === 'connected') {
307
+ clearTimeout(timeoutId);
308
+ const toast = document.getElementById('error-toast');
309
+ toast.style.display = 'none';
310
+ }
311
+ updateButtonState();
312
+ });
313
+
314
+ // Create data channel for messages
315
+ const dataChannel = peerConnection.createDataChannel('text');
316
+ dataChannel.onmessage = handleMessage;
317
+
318
+ // Create and send offer
319
+ const offer = await peerConnection.createOffer();
320
+ await peerConnection.setLocalDescription(offer);
321
+
322
+ await new Promise((resolve) => {
323
+ if (peerConnection.iceGatheringState === "complete") {
324
+ resolve();
325
+ } else {
326
+ const checkState = () => {
327
+ if (peerConnection.iceGatheringState === "complete") {
328
+ peerConnection.removeEventListener("icegatheringstatechange", checkState);
329
+ resolve();
330
+ }
331
+ };
332
+ peerConnection.addEventListener("icegatheringstatechange", checkState);
333
+ }
334
+ });
335
+
336
+ webrtc_id = Math.random().toString(36).substring(7);
337
+
338
+ const response = await fetch('/webrtc/offer', {
339
+ method: 'POST',
340
+ headers: { 'Content-Type': 'application/json' },
341
+ body: JSON.stringify({
342
+ sdp: peerConnection.localDescription.sdp,
343
+ type: peerConnection.localDescription.type,
344
+ webrtc_id: webrtc_id
345
+ })
346
+ });
347
+
348
+ const serverResponse = await response.json();
349
+
350
+ if (serverResponse.status === 'failed') {
351
+ showError(serverResponse.meta.error === 'concurrency_limit_reached'
352
+ ? `Too many connections. Maximum limit is ${serverResponse.meta.limit}`
353
+ : serverResponse.meta.error);
354
+ stop();
355
+ startButton.textContent = 'Start Recording';
356
+ return;
357
+ }
358
+
359
+ await peerConnection.setRemoteDescription(serverResponse);
360
+
361
+ // Create event stream to receive transcripts
362
+ eventSource = new EventSource('/transcript?webrtc_id=' + webrtc_id);
363
+ eventSource.addEventListener("output", (event) => {
364
+ appendTranscript(event.data);
365
+ });
366
+ } catch (err) {
367
+ clearTimeout(timeoutId);
368
+ console.error('Error setting up WebRTC:', err);
369
+ showError('Failed to establish connection. Please try again.');
370
+ stop();
371
+ startButton.textContent = 'Start Recording';
372
+ }
373
+ }
374
+
375
+ function appendTranscript(text) {
376
+ const p = document.createElement('p');
377
+ p.textContent = text;
378
+ transcriptDiv.appendChild(p);
379
+ transcriptDiv.scrollTop = transcriptDiv.scrollHeight;
380
+ }
381
+
382
+ function stop() {
383
+ if (animationFrame) {
384
+ cancelAnimationFrame(animationFrame);
385
+ }
386
+ if (audioContext) {
387
+ audioContext.close();
388
+ audioContext = null;
389
+ analyser = null;
390
+ audioSource = null;
391
+ }
392
+ if (peerConnection) {
393
+ if (peerConnection.getTransceivers) {
394
+ peerConnection.getTransceivers().forEach(transceiver => {
395
+ if (transceiver.stop) {
396
+ transceiver.stop();
397
+ }
398
+ });
399
+ }
400
+
401
+ if (peerConnection.getSenders) {
402
+ peerConnection.getSenders().forEach(sender => {
403
+ if (sender.track && sender.track.stop) sender.track.stop();
404
+ });
405
+ }
406
+
407
+ peerConnection.close();
408
+ peerConnection = null;
409
+ }
410
+ // Close EventSource connection
411
+ if (eventSource) {
412
+ eventSource.close();
413
+ eventSource = null;
414
+ }
415
+ audioLevel = 0;
416
+ updateButtonState();
417
+ }
418
+
419
+ startButton.addEventListener('click', () => {
420
+ if (startButton.textContent === 'Start Recording') {
421
+ setupWebRTC();
422
+ } else {
423
+ stop();
424
+ }
425
+ });
426
+ </script>
427
+ </body>
428
+
429
+ </html>