qfuxa commited on
Commit
d9feb41
·
1 Parent(s): 2b4a348

recording duration & waveform added

Browse files
Files changed (1) hide show
  1. web/live_transcription.html +166 -18
web/live_transcription.html CHANGED
@@ -13,22 +13,25 @@
13
  }
14
 
15
  #recordButton {
16
- width: 80px;
17
- height: 80px;
18
  border: none;
19
  border-radius: 50%;
20
  background-color: white;
21
  cursor: pointer;
22
- transition: background-color 0.3s ease, transform 0.2s ease;
23
  border: 1px solid rgb(233, 233, 233);
24
  display: flex;
25
  align-items: center;
26
  justify-content: center;
 
27
  }
28
 
29
  #recordButton.recording {
30
- border: 1px solid rgb(216, 182, 182);
31
- color: white;
 
 
32
  }
33
 
34
  #recordButton:active {
@@ -37,26 +40,59 @@
37
 
38
  /* Shape inside the button */
39
  .shape-container {
40
- width: 40px;
41
- height: 40px;
42
  display: flex;
43
  align-items: center;
44
  justify-content: center;
 
45
  }
46
 
47
  .shape {
48
- width: 40px;
49
- height: 40px;
50
  background-color: rgb(209, 61, 53);
51
  border-radius: 50%;
52
- transition: border-radius 0.3s ease, background-color 0.3s ease;
53
  }
54
 
55
  #recordButton.recording .shape {
56
- border-radius: 10px;
57
- width: 30px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  height: 30px;
 
 
 
 
 
59
 
 
 
 
 
 
 
 
 
 
 
60
  }
61
 
62
  #status {
@@ -107,7 +143,7 @@
107
  /* Speaker-labeled transcript area */
108
  #linesTranscript {
109
  margin: 20px auto;
110
- max-width: 600px;
111
  text-align: left;
112
  font-size: 16px;
113
  }
@@ -132,6 +168,8 @@
132
  border-radius: 8px 8px 8px 8px;
133
  padding: 2px 10px;
134
  margin-left: 10px;
 
 
135
  font-size: 14px;
136
  margin-bottom: 0px;
137
  color: rgb(134, 134, 134)
@@ -141,10 +179,12 @@
141
  background-color: #ffffff66;
142
  border-radius: 8px 8px 8px 8px;
143
  padding: 2px 10px;
 
 
144
  margin-left: 10px;
145
  font-size: 14px;
146
  margin-bottom: 0px;
147
- color: #7474746f
148
  }
149
 
150
  #timeInfo {
@@ -168,7 +208,7 @@
168
  }
169
 
170
  .buffer_transcription {
171
- color: #7474746f;
172
  margin-left: 4px;
173
  }
174
 
@@ -207,7 +247,6 @@
207
  padding: 2px 10px;
208
  font-size: 14px;
209
  margin-bottom: 0px;
210
-
211
  }
212
  </style>
213
  </head>
@@ -219,6 +258,12 @@
219
  <div class="shape-container">
220
  <div class="shape"></div>
221
  </div>
 
 
 
 
 
 
222
  </button>
223
  <div class="settings">
224
  <div>
@@ -251,12 +296,24 @@
251
  let chunkDuration = 1000;
252
  let websocketUrl = "ws://localhost:8000/asr";
253
  let userClosing = false;
 
 
 
 
 
 
 
 
 
 
 
254
 
255
  const statusText = document.getElementById("status");
256
  const recordButton = document.getElementById("recordButton");
257
  const chunkSelector = document.getElementById("chunkSelector");
258
  const websocketInput = document.getElementById("websocketInput");
259
  const linesTranscriptDiv = document.getElementById("linesTranscript");
 
260
 
261
  chunkSelector.addEventListener("change", () => {
262
  chunkDuration = parseInt(chunkSelector.value);
@@ -344,14 +401,17 @@
344
  }
345
 
346
  let textContent = item.text;
 
 
 
347
  if (idx === lines.length - 1 && buffer_diarization) {
348
  speakerLabel += `<span class="label_diarization"><span class="spinner"></span>Diarization lag<span id='timeInfo'>${remaining_time_diarization}s</span></span>`
349
  textContent += `<span class="buffer_diarization">${buffer_diarization}</span>`;
350
  }
351
- if (idx === lines.length - 1 && buffer_transcription) {
352
- speakerLabel += `<span class="label_transcription"><span class="spinner"></span>Transcription lag <span id='timeInfo'>${remaining_time_transcription}s</span></span>`
353
  textContent += `<span class="buffer_transcription">${buffer_transcription}</span>`;
354
  }
 
355
 
356
  return textContent
357
  ? `<p>${speakerLabel}<br/><div class='textcontent'>${textContent}</div></p>`
@@ -361,9 +421,59 @@
361
  linesTranscriptDiv.innerHTML = linesHtml;
362
  }
363
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
  async function startRecording() {
365
  try {
366
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
 
 
 
 
 
 
 
367
  recorder = new MediaRecorder(stream, { mimeType: "audio/webm" });
368
  recorder.ondataavailable = (e) => {
369
  if (websocket && websocket.readyState === WebSocket.OPEN) {
@@ -371,10 +481,16 @@
371
  }
372
  };
373
  recorder.start(chunkDuration);
 
 
 
 
 
374
  isRecording = true;
375
  updateUI();
376
  } catch (err) {
377
  statusText.textContent = "Error accessing microphone. Please allow microphone access.";
 
378
  }
379
  }
380
 
@@ -384,6 +500,37 @@
384
  recorder.stop();
385
  recorder = null;
386
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  isRecording = false;
388
 
389
  if (websocket) {
@@ -402,6 +549,7 @@
402
  await startRecording();
403
  } catch (err) {
404
  statusText.textContent = "Could not connect to WebSocket or access mic. Aborted.";
 
405
  }
406
  } else {
407
  stopRecording();
 
13
  }
14
 
15
  #recordButton {
16
+ width: 50px;
17
+ height: 50px;
18
  border: none;
19
  border-radius: 50%;
20
  background-color: white;
21
  cursor: pointer;
22
+ transition: all 0.3s ease;
23
  border: 1px solid rgb(233, 233, 233);
24
  display: flex;
25
  align-items: center;
26
  justify-content: center;
27
+ position: relative;
28
  }
29
 
30
  #recordButton.recording {
31
+ width: 180px;
32
+ border-radius: 40px;
33
+ justify-content: flex-start;
34
+ padding-left: 20px;
35
  }
36
 
37
  #recordButton:active {
 
40
 
41
  /* Shape inside the button */
42
  .shape-container {
43
+ width: 25px;
44
+ height: 25px;
45
  display: flex;
46
  align-items: center;
47
  justify-content: center;
48
+ flex-shrink: 0;
49
  }
50
 
51
  .shape {
52
+ width: 25px;
53
+ height: 25px;
54
  background-color: rgb(209, 61, 53);
55
  border-radius: 50%;
56
+ transition: all 0.3s ease;
57
  }
58
 
59
  #recordButton.recording .shape {
60
+ border-radius: 5px;
61
+ width: 25px;
62
+ height: 25px;
63
+ }
64
+
65
+ /* Recording elements */
66
+ .recording-info {
67
+ display: none;
68
+ align-items: center;
69
+ margin-left: 15px;
70
+ flex-grow: 1;
71
+ }
72
+
73
+ #recordButton.recording .recording-info {
74
+ display: flex;
75
+ }
76
+
77
+ .wave-container {
78
+ width: 60px;
79
  height: 30px;
80
+ position: relative;
81
+ display: flex;
82
+ align-items: center;
83
+ justify-content: center;
84
+ }
85
 
86
+ #waveCanvas {
87
+ width: 100%;
88
+ height: 100%;
89
+ }
90
+
91
+ .timer {
92
+ font-size: 14px;
93
+ font-weight: 500;
94
+ color: #333;
95
+ margin-left: 10px;
96
  }
97
 
98
  #status {
 
143
  /* Speaker-labeled transcript area */
144
  #linesTranscript {
145
  margin: 20px auto;
146
+ max-width: 700px;
147
  text-align: left;
148
  font-size: 16px;
149
  }
 
168
  border-radius: 8px 8px 8px 8px;
169
  padding: 2px 10px;
170
  margin-left: 10px;
171
+ display: inline-block;
172
+ white-space: nowrap;
173
  font-size: 14px;
174
  margin-bottom: 0px;
175
  color: rgb(134, 134, 134)
 
179
  background-color: #ffffff66;
180
  border-radius: 8px 8px 8px 8px;
181
  padding: 2px 10px;
182
+ display: inline-block;
183
+ white-space: nowrap;
184
  margin-left: 10px;
185
  font-size: 14px;
186
  margin-bottom: 0px;
187
+ color: #000000
188
  }
189
 
190
  #timeInfo {
 
208
  }
209
 
210
  .buffer_transcription {
211
+ color: #7474748c;
212
  margin-left: 4px;
213
  }
214
 
 
247
  padding: 2px 10px;
248
  font-size: 14px;
249
  margin-bottom: 0px;
 
250
  }
251
  </style>
252
  </head>
 
258
  <div class="shape-container">
259
  <div class="shape"></div>
260
  </div>
261
+ <div class="recording-info">
262
+ <div class="wave-container">
263
+ <canvas id="waveCanvas"></canvas>
264
+ </div>
265
+ <div class="timer">00:00</div>
266
+ </div>
267
  </button>
268
  <div class="settings">
269
  <div>
 
296
  let chunkDuration = 1000;
297
  let websocketUrl = "ws://localhost:8000/asr";
298
  let userClosing = false;
299
+ let startTime = null;
300
+ let timerInterval = null;
301
+ let audioContext = null;
302
+ let analyser = null;
303
+ let microphone = null;
304
+ let waveCanvas = document.getElementById("waveCanvas");
305
+ let waveCtx = waveCanvas.getContext("2d");
306
+ let animationFrame = null;
307
+ waveCanvas.width = 60 * (window.devicePixelRatio || 1);
308
+ waveCanvas.height = 30 * (window.devicePixelRatio || 1);
309
+ waveCtx.scale(window.devicePixelRatio || 1, window.devicePixelRatio || 1);
310
 
311
  const statusText = document.getElementById("status");
312
  const recordButton = document.getElementById("recordButton");
313
  const chunkSelector = document.getElementById("chunkSelector");
314
  const websocketInput = document.getElementById("websocketInput");
315
  const linesTranscriptDiv = document.getElementById("linesTranscript");
316
+ const timerElement = document.querySelector(".timer");
317
 
318
  chunkSelector.addEventListener("change", () => {
319
  chunkDuration = parseInt(chunkSelector.value);
 
401
  }
402
 
403
  let textContent = item.text;
404
+ if (idx === lines.length - 1) {
405
+ speakerLabel += `<span class="label_transcription"><span class="spinner"></span>Transcription lag <span id='timeInfo'>${remaining_time_transcription}s</span></span>`
406
+ }
407
  if (idx === lines.length - 1 && buffer_diarization) {
408
  speakerLabel += `<span class="label_diarization"><span class="spinner"></span>Diarization lag<span id='timeInfo'>${remaining_time_diarization}s</span></span>`
409
  textContent += `<span class="buffer_diarization">${buffer_diarization}</span>`;
410
  }
411
+ if (idx === lines.length - 1) {
 
412
  textContent += `<span class="buffer_transcription">${buffer_transcription}</span>`;
413
  }
414
+
415
 
416
  return textContent
417
  ? `<p>${speakerLabel}<br/><div class='textcontent'>${textContent}</div></p>`
 
421
  linesTranscriptDiv.innerHTML = linesHtml;
422
  }
423
 
424
+ function updateTimer() {
425
+ if (!startTime) return;
426
+
427
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
428
+ const minutes = Math.floor(elapsed / 60).toString().padStart(2, "0");
429
+ const seconds = (elapsed % 60).toString().padStart(2, "0");
430
+ timerElement.textContent = `${minutes}:${seconds}`;
431
+ }
432
+
433
+ function drawWaveform() {
434
+ if (!analyser) return;
435
+
436
+ const bufferLength = analyser.frequencyBinCount;
437
+ const dataArray = new Uint8Array(bufferLength);
438
+ analyser.getByteTimeDomainData(dataArray);
439
+
440
+ waveCtx.clearRect(0, 0, waveCanvas.width / (window.devicePixelRatio || 1), waveCanvas.height / (window.devicePixelRatio || 1));
441
+ waveCtx.lineWidth = 1;
442
+ waveCtx.strokeStyle = 'rgb(0, 0, 0)';
443
+ waveCtx.beginPath();
444
+
445
+ const sliceWidth = (waveCanvas.width / (window.devicePixelRatio || 1)) / bufferLength;
446
+ let x = 0;
447
+
448
+ for (let i = 0; i < bufferLength; i++) {
449
+ const v = dataArray[i] / 128.0;
450
+ const y = v * (waveCanvas.height / (window.devicePixelRatio || 1)) / 2;
451
+
452
+ if (i === 0) {
453
+ waveCtx.moveTo(x, y);
454
+ } else {
455
+ waveCtx.lineTo(x, y);
456
+ }
457
+
458
+ x += sliceWidth;
459
+ }
460
+
461
+ waveCtx.lineTo(waveCanvas.width / (window.devicePixelRatio || 1), waveCanvas.height / (window.devicePixelRatio || 1) / 2);
462
+ waveCtx.stroke();
463
+
464
+ animationFrame = requestAnimationFrame(drawWaveform);
465
+ }
466
+
467
  async function startRecording() {
468
  try {
469
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
470
+
471
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
472
+ analyser = audioContext.createAnalyser();
473
+ analyser.fftSize = 256;
474
+ microphone = audioContext.createMediaStreamSource(stream);
475
+ microphone.connect(analyser);
476
+
477
  recorder = new MediaRecorder(stream, { mimeType: "audio/webm" });
478
  recorder.ondataavailable = (e) => {
479
  if (websocket && websocket.readyState === WebSocket.OPEN) {
 
481
  }
482
  };
483
  recorder.start(chunkDuration);
484
+
485
+ startTime = Date.now();
486
+ timerInterval = setInterval(updateTimer, 1000);
487
+ drawWaveform();
488
+
489
  isRecording = true;
490
  updateUI();
491
  } catch (err) {
492
  statusText.textContent = "Error accessing microphone. Please allow microphone access.";
493
+ console.error(err);
494
  }
495
  }
496
 
 
500
  recorder.stop();
501
  recorder = null;
502
  }
503
+
504
+ if (microphone) {
505
+ microphone.disconnect();
506
+ microphone = null;
507
+ }
508
+
509
+ if (analyser) {
510
+ analyser = null;
511
+ }
512
+
513
+ if (audioContext && audioContext.state !== 'closed') {
514
+ try {
515
+ audioContext.close();
516
+ } catch (e) {
517
+ console.warn("Could not close audio context:", e);
518
+ }
519
+ audioContext = null;
520
+ }
521
+
522
+ if (animationFrame) {
523
+ cancelAnimationFrame(animationFrame);
524
+ animationFrame = null;
525
+ }
526
+
527
+ if (timerInterval) {
528
+ clearInterval(timerInterval);
529
+ timerInterval = null;
530
+ }
531
+ timerElement.textContent = "00:00";
532
+ startTime = null;
533
+
534
  isRecording = false;
535
 
536
  if (websocket) {
 
549
  await startRecording();
550
  } catch (err) {
551
  statusText.textContent = "Could not connect to WebSocket or access mic. Aborted.";
552
+ console.error(err);
553
  }
554
  } else {
555
  stopRecording();