speaker-detector 0.1.5__py3-none-any.whl → 0.1.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. speaker_detector/cli.py +12 -26
  2. speaker_detector/constants.py +10 -0
  3. speaker_detector/core.py +80 -65
  4. speaker_detector/model/classifier.ckpt +0 -0
  5. speaker_detector/model/embedding_model.ckpt +0 -0
  6. speaker_detector/model/hyperparams.yaml +58 -0
  7. speaker_detector/model/label_encoder.ckpt +7207 -0
  8. speaker_detector/model/mean_var_norm_emb.ckpt +0 -0
  9. speaker_detector/server copy.py +296 -0
  10. speaker_detector/server.py +95 -0
  11. speaker_detector/speaker_state.py +103 -0
  12. speaker_detector/web/static/favicon.ico +0 -0
  13. speaker_detector/web/static/index.html +29 -0
  14. speaker_detector/web/static/scripts/loader copy.js +10 -0
  15. speaker_detector/web/static/scripts/loader.js +14 -0
  16. speaker_detector/web/static/scripts/script copy.js +954 -0
  17. speaker_detector/web/static/scripts/script.js +22 -0
  18. speaker_detector/web/static/style.css +133 -0
  19. {speaker_detector-0.1.5.dist-info → speaker_detector-0.1.7.dist-info}/METADATA +28 -3
  20. speaker_detector-0.1.7.dist-info/RECORD +26 -0
  21. {speaker_detector-0.1.5.dist-info → speaker_detector-0.1.7.dist-info}/WHEEL +1 -1
  22. speaker_detector/analyze.py +0 -59
  23. speaker_detector/combine.py +0 -22
  24. speaker_detector/export_embeddings.py +0 -62
  25. speaker_detector/export_model.py +0 -40
  26. speaker_detector/generate_summary.py +0 -110
  27. speaker_detector-0.1.5.dist-info/RECORD +0 -15
  28. /speaker_detector/{ECAPA_TDNN.py → model/ECAPA_TDNN.py} +0 -0
  29. /speaker_detector/{__init__.py → web/static/__init__.py} +0 -0
  30. {speaker_detector-0.1.5.dist-info → speaker_detector-0.1.7.dist-info}/entry_points.txt +0 -0
  31. {speaker_detector-0.1.5.dist-info → speaker_detector-0.1.7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,954 @@
1
+
2
+ console.log("✅ Full script.js loaded");
3
+
4
+ let knownSpeakers = [];
5
+ let meetingMediaRecorder = null;
6
+ let meetingBlob = null;
7
+ let meetingId = null;
8
+
9
+ window.addEventListener("DOMContentLoaded", () => {
10
+ setupAccordionUI();
11
+ setupActions();
12
+ fetchSpeakers();
13
+ fetchRecordings();
14
+ // fetchExports();
15
+ fetchMeetings();
16
+ });
17
+
18
+ function formatTime(sec) {
19
+ const m = Math.floor(sec / 60);
20
+ const s = String(Math.floor(sec % 60)).padStart(2, '0');
21
+ return `${m}:${s}`;
22
+ }
23
+
24
+ function setupAccordionUI() {
25
+ document.querySelectorAll('.accordion-step').forEach(step => {
26
+ step.addEventListener('click', () => {
27
+ document.querySelectorAll('.accordion-step').forEach(s => s.classList.remove('active'));
28
+ document.querySelectorAll('.accordion-content').forEach(c => c.classList.remove('active'));
29
+ step.classList.add('active');
30
+ document.getElementById(step.dataset.tab).classList.add('active');
31
+ });
32
+ });
33
+ }
34
+
35
+ function setupActions() {
36
+ document.getElementById('action-btn')?.addEventListener('click', runMicTest);
37
+ document.getElementById('enroll-speaker-btn')?.addEventListener('click', enrollSpeaker);
38
+ document.getElementById('identify-speaker-btn')?.addEventListener('click', identifySpeaker);
39
+ document.getElementById('start-meeting')?.addEventListener('click', startMeeting);
40
+ document.getElementById('stop-meeting')?.addEventListener('click', stopMeeting);
41
+ document.getElementById('record-bg-btn')?.addEventListener('click', recordBackgroundNoise);
42
+
43
+ // ✅ Show mic popup trigger (e.g. from test button or some icon)
44
+ const micPopup = document.querySelector(".mic-popup");
45
+ const micStartBtn = document.getElementById("mic-start-btn");
46
+ const micCancelBtn = document.getElementById("mic-cancel-btn");
47
+
48
+ // Just for demo: automatically show popup on start
49
+ // remove this later and call `micPopup.classList.remove("hidden")` manually as needed
50
+
51
+ micStartBtn?.addEventListener("click", () => {
52
+ alert("🎤 Mic start clicked. Add your recording logic here.");
53
+ micPopup?.classList.add("hidden");
54
+ });
55
+
56
+ micCancelBtn?.addEventListener("click", () => {
57
+ micPopup?.classList.add("hidden");
58
+ });
59
+ }
60
+
61
+
62
+ function recordBackgroundNoise() {
63
+ const statusEl = document.getElementById("record-bg-status");
64
+ statusEl.textContent = "🎙 Recording background noise...";
65
+
66
+ navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
67
+ const recorder = new MediaRecorder(stream, { mimeType: "audio/webm;codecs=opus" });
68
+ const chunks = [];
69
+
70
+ recorder.ondataavailable = e => chunks.push(e.data);
71
+
72
+ recorder.onstop = async () => {
73
+ const blob = new Blob(chunks, { type: "audio/webm" });
74
+ const form = new FormData();
75
+ form.append("audio", blob, `noise_${Date.now()}.webm`);
76
+
77
+ try {
78
+ const res = await fetch("/api/background_noise", { method: "POST", body: form });
79
+ const data = await res.json();
80
+ statusEl.textContent = data.success ? "✅ Background noise saved." : `❌ ${data.error}`;
81
+ } catch (err) {
82
+ console.error("❌ Upload failed:", err);
83
+ statusEl.textContent = "❌ Failed to save noise.";
84
+ }
85
+
86
+ stream.getTracks().forEach(t => t.stop());
87
+ };
88
+
89
+ recorder.start();
90
+ setTimeout(() => recorder.stop(), 3000);
91
+ });
92
+ }
93
+
94
+
95
+ function getSpeakerPrompt() {
96
+ return `
97
+ Please read the following aloud for speaker enrollment:
98
+ "The quick brown fox jumps over the lazy dog. This sentence contains every letter of the alphabet, offering a rich variety of sounds. Speak naturally, with your normal tone and pace."
99
+ `.trim();
100
+ }
101
+
102
+
103
+ function runMicTest() {
104
+ const resultEl = document.getElementById('identify-result');
105
+ resultEl.innerHTML = "Testing microphone...";
106
+
107
+ navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
108
+ const canvas = document.querySelector('.accordion-content.active .visualizer');
109
+ if (canvas) setupVisualizer(stream, canvas);
110
+
111
+ const recorder = new MediaRecorder(stream);
112
+ const chunks = [];
113
+
114
+ recorder.ondataavailable = e => chunks.push(e.data);
115
+
116
+ recorder.onstop = () => {
117
+ const blob = new Blob(chunks, { type: 'audio/webm' });
118
+ const url = URL.createObjectURL(blob);
119
+
120
+ resultEl.innerHTML = `✅ Mic test successful.<br><audio controls src="${url}"></audio>`;
121
+
122
+ const micTestStatus = document.getElementById('mic-test-status');
123
+ if (micTestStatus) micTestStatus.textContent = "✅ Passed";
124
+
125
+ stream.getTracks().forEach(t => t.stop());
126
+ };
127
+
128
+ recorder.start();
129
+ setTimeout(() => recorder.stop(), 3000);
130
+ });
131
+ }
132
+
133
+
134
+ function setupVisualizer(stream, canvas) {
135
+ const audioCtx = new AudioContext();
136
+ const analyser = audioCtx.createAnalyser();
137
+ const source = audioCtx.createMediaStreamSource(stream);
138
+ source.connect(analyser);
139
+
140
+ const canvasCtx = canvas.getContext('2d');
141
+ analyser.fftSize = 2048;
142
+ const bufferLength = analyser.frequencyBinCount;
143
+ const dataArray = new Uint8Array(bufferLength);
144
+
145
+ function draw() {
146
+ requestAnimationFrame(draw);
147
+ analyser.getByteTimeDomainData(dataArray);
148
+
149
+ canvasCtx.fillStyle = '#111';
150
+ canvasCtx.fillRect(0, 0, canvas.width, canvas.height);
151
+
152
+ canvasCtx.lineWidth = 2;
153
+ canvasCtx.strokeStyle = 'lime';
154
+ canvasCtx.beginPath();
155
+
156
+ const sliceWidth = canvas.width / bufferLength;
157
+ let x = 0;
158
+
159
+ for (let i = 0; i < bufferLength; i++) {
160
+ const v = dataArray[i] / 128.0;
161
+ const y = (v * canvas.height) / 2;
162
+ if (i === 0) canvasCtx.moveTo(x, y);
163
+ else canvasCtx.lineTo(x, y);
164
+ x += sliceWidth;
165
+ }
166
+
167
+ canvasCtx.lineTo(canvas.width, canvas.height / 2);
168
+ canvasCtx.stroke();
169
+ }
170
+
171
+ draw();
172
+ }
173
+
174
+ function enrollSpeaker() {
175
+ const id = document.getElementById("speaker-id").value.trim();
176
+ if (!id) return alert("Please enter speaker ID");
177
+
178
+ navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
179
+ const recorder = new MediaRecorder(stream, { mimeType: "audio/webm;codecs=opus" });
180
+ const chunks = [];
181
+
182
+ recorder.ondataavailable = e => chunks.push(e.data);
183
+
184
+ recorder.onstop = async () => {
185
+ const blob = new Blob(chunks);
186
+ const url = URL.createObjectURL(blob);
187
+
188
+ // ✅ Show preview UI
189
+ const previewDiv = document.createElement("div");
190
+ previewDiv.innerHTML = `
191
+ <h4>🎧 Preview your enrollment for "${id}"</h4>
192
+ <audio controls src="${url}"></audio>
193
+ <button id="confirm-enroll-btn">✅ Confirm Enrollment</button>
194
+ <button id="discard-enroll-btn">❌ Discard</button>
195
+ `;
196
+ document.body.appendChild(previewDiv);
197
+
198
+ document.getElementById("confirm-enroll-btn").onclick = async () => {
199
+ const form = new FormData();
200
+ form.append("file", blob, `enroll_${Date.now()}.webm`);
201
+
202
+ const res = await fetch(`/api/enroll/${encodeURIComponent(id)}`, {
203
+ method: "POST",
204
+ body: form
205
+ });
206
+
207
+ const data = await res.json();
208
+ if (data.status === "enrolled") {
209
+ alert(`✅ Enrolled "${id}".`);
210
+ fetchSpeakers();
211
+ } else {
212
+ alert(`❌ Enroll failed: ${data.error}`);
213
+ }
214
+
215
+ previewDiv.remove();
216
+ };
217
+
218
+ document.getElementById("discard-enroll-btn").onclick = () => {
219
+ alert("🚫 Discarded recording.");
220
+ previewDiv.remove();
221
+ };
222
+
223
+ stream.getTracks().forEach(t => t.stop());
224
+ };
225
+
226
+ recorder.start();
227
+ alert("🎙️ Recording for 20 seconds. Please read the provided text aloud...");
228
+ setTimeout(() => recorder.stop(), 20000);
229
+ });
230
+ }
231
+
232
+ function renameSpeaker(oldName) {
233
+ const newName = prompt(`Rename "${oldName}" to:`, oldName);
234
+ if (!newName || newName === oldName) return;
235
+
236
+ fetch(`/api/speakers/rename`, {
237
+ method: "POST",
238
+ headers: { "Content-Type": "application/json" },
239
+ body: JSON.stringify({ oldName, newName })
240
+ })
241
+ .then(res => res.json())
242
+ .then(data => {
243
+ if (data.success) {
244
+ alert(`✅ Renamed to "${newName}".`);
245
+ fetchSpeakers();
246
+ } else {
247
+ alert(`❌ Rename failed: ${data.error}`);
248
+ }
249
+ });
250
+ }
251
+
252
+ function deleteSpeaker(speakerId) {
253
+ if (!confirm(`Are you sure you want to delete "${speakerId}"?`)) return;
254
+
255
+ fetch(`/api/speakers/${encodeURIComponent(speakerId)}`, { method: "DELETE" })
256
+ .then(res => res.json())
257
+ .then(data => {
258
+ if (data.deleted) {
259
+ alert(`🗑️ Deleted "${speakerId}"`);
260
+ fetchSpeakers();
261
+ } else {
262
+ alert(`❌ Delete failed: ${data.error}`);
263
+ }
264
+ });
265
+ }
266
+
267
+ function improveSpeaker(speakerId) {
268
+ showMicOverlay({
269
+ title: `🔁 Improve Speaker: "${speakerId}"`,
270
+ message: getSpeakerPrompt(),
271
+ countdownSeconds: 5,
272
+ onStop: (blob) => {
273
+ const url = URL.createObjectURL(blob);
274
+ const overlayContent = document.querySelector(".overlay-content");
275
+
276
+ overlayContent.innerHTML = `
277
+ <h3>✅ Sample Ready for "${speakerId}"</h3>
278
+ <audio controls src="${url}"></audio>
279
+ <p>Preview your voice sample. If you're happy, click Upload:</p>
280
+ <button id="confirm-improve-btn">✅ Upload</button>
281
+ <button id="discard-improve-btn">❌ Discard</button>
282
+ `;
283
+
284
+ document.getElementById("confirm-improve-btn").onclick = async () => {
285
+ const form = new FormData();
286
+ form.append("file", blob, `improve_${Date.now()}.webm`);
287
+
288
+ const res = await fetch(`/api/speakers/${encodeURIComponent(speakerId)}/improve`, {
289
+ method: "POST",
290
+ body: form,
291
+ });
292
+
293
+ const data = await res.json();
294
+ if (data.status === "improved") {
295
+ alert(`✅ Improved recording added to "${speakerId}".`);
296
+ } else {
297
+ alert(`❌ Improve failed: ${data.error}`);
298
+ }
299
+
300
+ closeMicOverlay();
301
+ };
302
+
303
+ document.getElementById("discard-improve-btn").onclick = () => {
304
+ alert("🚫 Discarded recording.");
305
+ closeMicOverlay();
306
+ };
307
+ },
308
+ onStreamReady: (stream, stopRecorder) => {
309
+ const countdownEl = document.querySelector("#mic-countdown");
310
+ let duration = 10;
311
+ const recorder = new MediaRecorder(stream, { mimeType: "audio/webm;codecs=opus" });
312
+ const chunks = [];
313
+
314
+ recorder.ondataavailable = (e) => chunks.push(e.data);
315
+ recorder.onstop = () => stopRecorder(new Blob(chunks, { type: "audio/webm" }));
316
+
317
+ recorder.start();
318
+
319
+ const interval = setInterval(() => {
320
+ countdownEl.textContent = `🎙️ Recording... ${duration--}s left`;
321
+ if (duration < 0) {
322
+ clearInterval(interval);
323
+ recorder.stop();
324
+ }
325
+ }, 1000);
326
+ },
327
+ });
328
+ }
329
+
330
+ async function identifySpeaker() {
331
+ const resultEl = document.getElementById('identify-result-step-3') || document.getElementById('identify-result');
332
+ const canvas = document.querySelector('#step-3 .visualizer');
333
+ const promptText = getSpeakerPrompt();
334
+
335
+ // Show prompt and prepare UI
336
+ resultEl.innerHTML = `
337
+ <p>${promptText}</p>
338
+ <p>🎙️ Preparing to record for speaker identification...</p>
339
+ `;
340
+
341
+ try {
342
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
343
+ if (canvas) setupVisualizer(stream, canvas);
344
+
345
+ // Countdown
346
+ const countdownEl = document.createElement('div');
347
+ countdownEl.textContent = "Recording will start in 3...";
348
+ resultEl.appendChild(countdownEl);
349
+
350
+ await delayCountdown(countdownEl, 3);
351
+
352
+ // Recording for 5s (you can adjust this)
353
+ const recorder = new MediaRecorder(stream, { mimeType: "audio/webm;codecs=opus" });
354
+ const chunks = [];
355
+
356
+ recorder.ondataavailable = e => chunks.push(e.data);
357
+
358
+ recorder.onstop = async () => {
359
+ const blob = new Blob(chunks);
360
+ const url = URL.createObjectURL(blob);
361
+
362
+ // Send to backend
363
+ resultEl.innerHTML = `<p>⏳ Sending to backend...</p>`;
364
+ const form = new FormData();
365
+ form.append("file", blob, "identify.webm");
366
+
367
+ try {
368
+ const res = await fetch("/api/identify", { method: "POST", body: form });
369
+ const { speaker, score, error } = await res.json();
370
+
371
+ resultEl.innerHTML = error
372
+ ? `❌ ${error}`
373
+ : `🗣️ Speaker: <strong class="segment-speaker">${speaker}</strong> (score: ${score})<br><audio controls src="${url}"></audio>`;
374
+
375
+ // Correction button
376
+ const feedbackBtn = document.createElement("button");
377
+ feedbackBtn.textContent = "✏️ Correct Speaker";
378
+ feedbackBtn.style.marginLeft = "10px";
379
+ feedbackBtn.onclick = () => {
380
+ showCorrectionUI(blob, resultEl);
381
+ };
382
+ resultEl.appendChild(feedbackBtn);
383
+
384
+ } catch (err) {
385
+ console.error(err);
386
+ resultEl.innerHTML = `❌ Failed to identify speaker.`;
387
+ }
388
+
389
+ stream.getTracks().forEach(t => t.stop());
390
+ };
391
+
392
+ recorder.start();
393
+ countdownEl.textContent = "🎙️ Recording... Speak now.";
394
+ setTimeout(() => recorder.stop(), 5000);
395
+
396
+ } catch (err) {
397
+ console.error(err);
398
+ resultEl.innerHTML = "❌ Failed to access microphone.";
399
+ }
400
+ }
401
+
402
+ // Utility to delay with countdown
403
+ function delayCountdown(el, seconds) {
404
+ return new Promise(resolve => {
405
+ let count = seconds;
406
+ const interval = setInterval(() => {
407
+ el.textContent = `Recording will start in ${count}...`;
408
+ count--;
409
+ if (count < 0) {
410
+ clearInterval(interval);
411
+ resolve();
412
+ }
413
+ }, 1000);
414
+ });
415
+ }
416
+
417
+ // Shared correction UI
418
+ function showCorrectionUI(blob, container) {
419
+ const wrapper = document.createElement("div");
420
+ wrapper.style.marginTop = "0.5rem";
421
+
422
+ const label = document.createElement("label");
423
+ label.textContent = "Correct speaker: ";
424
+
425
+ const input = document.createElement("input");
426
+ input.placeholder = "e.g. Lara or new...";
427
+ input.style.width = "200px";
428
+
429
+ const confirmBtn = document.createElement("button");
430
+ confirmBtn.textContent = "✅ Confirm";
431
+ confirmBtn.style.marginLeft = "0.5rem";
432
+
433
+ confirmBtn.onclick = async () => {
434
+ const correctedName = input.value.trim();
435
+ if (!correctedName) return alert("Please enter a name.");
436
+
437
+ const uploadForm = new FormData();
438
+ uploadForm.append("file", blob, `identify_${Date.now()}.webm`);
439
+
440
+ const res = await fetch(`/api/enroll/${encodeURIComponent(correctedName)}`, {
441
+ method: "POST",
442
+ body: uploadForm,
443
+ });
444
+
445
+ const data = await res.json();
446
+ if (data.status === "enrolled") {
447
+ alert(`✅ Reclassified and enrolled as "${correctedName}".`);
448
+ fetchSpeakers();
449
+ } else {
450
+ alert("❌ Correction failed.");
451
+ }
452
+ };
453
+
454
+ wrapper.appendChild(label);
455
+ wrapper.appendChild(input);
456
+ wrapper.appendChild(confirmBtn);
457
+ container.appendChild(wrapper);
458
+ }
459
+
460
+ // function exportSpeakersJSON() {
461
+ // fetch("/api/export-speakers-json", { method: "POST" }).then(res => {
462
+ // document.getElementById("export-json-status").textContent = res.ok ? "✅ Combined & Exported" : "❌ Failed";
463
+ // fetchExports();
464
+ // });
465
+ // }
466
+
467
+ function fetchSpeakers() {
468
+ fetch("/api/speakers")
469
+ .then(res => res.json())
470
+ .then(async (data) => {
471
+ const list = document.getElementById("speakers-list");
472
+ if (!list) return;
473
+
474
+ if (!Array.isArray(data) || data.length === 0) {
475
+ list.innerHTML = "<li><em>No speakers enrolled.</em></li>";
476
+ return;
477
+ }
478
+
479
+ // Get list of speakers needing rebuild
480
+ let needsRebuild = [];
481
+ try {
482
+ const res = await fetch("/api/speakers/needs-rebuild");
483
+ const result = await res.json();
484
+ needsRebuild = result.toRebuild || [];
485
+ } catch {
486
+ console.warn("⚠️ Could not fetch rebuild info");
487
+ }
488
+
489
+ list.innerHTML = "";
490
+
491
+ data.forEach(({ name, recordings }) => {
492
+ const li = document.createElement("li");
493
+ const needsUpdate = needsRebuild.includes(name);
494
+
495
+ li.innerHTML = `
496
+ <strong>${name}</strong> (${recordings} recording${recordings !== 1 ? "s" : ""})
497
+ ${needsUpdate ? '<span style="color: #0ff;"> 🔁 Needs rebuild</span>' : ""}
498
+ <button onclick="renameSpeaker('${name}')">✏️ Rename</button>
499
+ <button onclick="deleteSpeaker('${name}')">🗑️ Delete</button>
500
+ <button onclick="improveSpeaker('${name}')">🔁 Improve</button>
501
+ ${
502
+ needsUpdate
503
+ ? `<button onclick="rebuildSpeaker('${name}')" style="margin-left: 6px;">⚙️ Rebuild</button>`
504
+ : ""
505
+ }
506
+ `;
507
+
508
+ list.appendChild(li);
509
+ });
510
+ })
511
+ .catch(err => {
512
+ console.error("❌ Failed to fetch speakers:", err);
513
+ const list = document.getElementById("speakers-list");
514
+ if (list) {
515
+ list.innerHTML = "<li><em>Error loading speakers.</em></li>";
516
+ }
517
+ });
518
+ }
519
+
520
+ function showMicOverlay({ title, message, countdownSeconds = 3, onStop, onStreamReady }) {
521
+ // Create overlay
522
+ const overlay = document.createElement("div");
523
+ overlay.className = "mic-overlay";
524
+ overlay.innerHTML = `
525
+ <div class="overlay-content">
526
+ <h2>${title}</h2>
527
+ <p>${message}</p>
528
+ <p id="mic-countdown">⏳ Starting in ${countdownSeconds}...</p>
529
+ <button id="cancel-overlay">❌ Cancel</button>
530
+ </div>
531
+ `;
532
+ document.body.appendChild(overlay);
533
+
534
+ document.getElementById("cancel-overlay").onclick = () => {
535
+ overlay.remove();
536
+ };
537
+
538
+ // Request microphone access and prepare recorder
539
+ navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
540
+ const countdownEl = document.getElementById("mic-countdown");
541
+ let count = countdownSeconds;
542
+
543
+ const interval = setInterval(() => {
544
+ countdownEl.textContent = `⏳ Starting in ${count--}...`;
545
+ if (count < 0) {
546
+ clearInterval(interval);
547
+ countdownEl.textContent = "🎙️ Recording...";
548
+ const recorder = new MediaRecorder(stream, { mimeType: "audio/webm;codecs=opus" });
549
+ const chunks = [];
550
+
551
+ recorder.ondataavailable = (e) => chunks.push(e.data);
552
+
553
+ recorder.onstop = () => {
554
+ const blob = new Blob(chunks, { type: "audio/webm" });
555
+ onStop?.(blob);
556
+ };
557
+
558
+ onStreamReady?.(stream, () => recorder.stop());
559
+
560
+ recorder.start();
561
+
562
+ // Default 20s stop if no manual trigger
563
+ setTimeout(() => {
564
+ if (recorder.state === "recording") recorder.stop();
565
+ }, 20000);
566
+ }
567
+ }, 1000);
568
+ });
569
+ }
570
+
571
+ function closeMicOverlay() {
572
+ document.querySelector(".mic-overlay")?.remove();
573
+ }
574
+
575
+ function rebuildSpeaker(name) {
576
+ const list = document.getElementById("speakers-list");
577
+ const button = [...list.querySelectorAll("button")]
578
+ .find(btn => btn.textContent.includes("Rebuild") && btn.onclick?.toString().includes(name));
579
+ const tag = [...list.querySelectorAll("span")]
580
+ .find(span => span.textContent.includes("Needs rebuild") && span.parentElement?.textContent.includes(name));
581
+
582
+ if (!confirm(`Rebuild embedding for "${name}"?`)) return;
583
+
584
+ if (button) {
585
+ button.disabled = true;
586
+ button.innerHTML = `⚙️ Rebuilding <span class="spinner"></span>`;
587
+ }
588
+ if (tag) {
589
+ tag.textContent = "🔁 Rebuilding...";
590
+ tag.style.color = "gray";
591
+ tag.style.opacity = "0.7";
592
+ }
593
+
594
+ fetch(`/api/rebuild/${encodeURIComponent(name)}`, { method: "POST" })
595
+ .then(res => res.json())
596
+ .then(data => {
597
+ if (data.status === "rebuilt") {
598
+ alert(`✅ Rebuilt embedding for "${data.name}".`);
599
+ } else {
600
+ alert(`❌ Failed to rebuild: ${data.error || JSON.stringify(data)}`);
601
+ }
602
+ })
603
+ .catch(err => {
604
+ alert(`❌ Error: ${err.message}`);
605
+ })
606
+ .finally(() => {
607
+ if (button) {
608
+ button.disabled = false;
609
+ button.textContent = "⚙️ Rebuild";
610
+ }
611
+ fetchSpeakers(); // Refresh status
612
+ });
613
+ }
614
+
615
+ function improveSpeaker(speakerId) {
616
+ showMicOverlay({
617
+ title: `🔁 Improve Speaker: "${speakerId}"`,
618
+ message: getSpeakerPrompt(),
619
+ countdownSeconds: 20,
620
+ onStop: (blob) => {
621
+ const url = URL.createObjectURL(blob);
622
+
623
+ const previewDiv = document.createElement("div");
624
+ previewDiv.classList.add("overlay-content");
625
+ previewDiv.innerHTML = `
626
+ <h4>🎧 Preview your improved recording for "${speakerId}"</h4>
627
+ <audio controls src="${url}"></audio>
628
+ <button id="confirm-improve-btn">✅ Confirm Upload</button>
629
+ <button id="discard-improve-btn">❌ Discard</button>
630
+ `;
631
+ document.body.appendChild(previewDiv);
632
+
633
+ document.getElementById("confirm-improve-btn").onclick = async () => {
634
+ const form = new FormData();
635
+ form.append("file", blob, `improve_${Date.now()}.webm`);
636
+
637
+ const res = await fetch(`/api/speakers/${encodeURIComponent(speakerId)}/improve`, {
638
+ method: "POST",
639
+ body: form,
640
+ });
641
+
642
+ const data = await res.json();
643
+ if (data.status === "improved") {
644
+ alert(`✅ Improved recording added to "${speakerId}".`);
645
+ } else {
646
+ alert(`❌ Improve failed: ${data.error}`);
647
+ }
648
+
649
+ previewDiv.remove();
650
+ closeMicOverlay();
651
+ };
652
+
653
+ document.getElementById("discard-improve-btn").onclick = () => {
654
+ alert("🚫 Discarded recording.");
655
+ previewDiv.remove();
656
+ closeMicOverlay();
657
+ };
658
+ },
659
+ });
660
+ }
661
+
662
+ function renameSpeaker(id) {
663
+ const newName = prompt(`Rename speaker "${id}" to:`);
664
+ if (!newName || newName === id) return;
665
+
666
+ fetch(`/api/speakers/rename`, {
667
+ method: "POST",
668
+ headers: { "Content-Type": "application/json" },
669
+ body: JSON.stringify({ oldName: id, newName }),
670
+ })
671
+ .then(res => res.json())
672
+ .then(data => {
673
+ if (data.status === "renamed") {
674
+ alert(`✅ Renamed to ${data.to}`);
675
+ fetchSpeakers();
676
+ } else {
677
+ alert(`❌ Failed: ${data.error}`);
678
+ }
679
+ });
680
+ }
681
+
682
+ function deleteSpeaker(id) {
683
+ if (!confirm(`Delete speaker "${id}"?`)) return;
684
+
685
+ fetch(`/api/speakers/${encodeURIComponent(id)}`, { method: "DELETE" })
686
+ .then(res => res.json())
687
+ .then(data => {
688
+ if (data.deleted) {
689
+ alert(`✅ Deleted "${id}"`);
690
+ fetchSpeakers();
691
+ } else {
692
+ alert(`❌ Failed`);
693
+ }
694
+ });
695
+ }
696
+
697
+ function downloadExport(filename) {
698
+ const link = document.createElement('a');
699
+ link.href = `/exports/${filename}`;
700
+ link.download = filename;
701
+ link.click();
702
+ }
703
+
704
+ function deleteExport(filename) {
705
+ if (!confirm(`Delete ${filename}?`)) return;
706
+
707
+ fetch(`/api/delete-export/${encodeURIComponent(filename)}`, { method: 'DELETE' })
708
+ .then(res => res.json())
709
+ .then(data => {
710
+ if (data.deleted) {
711
+ alert(`✅ Deleted ${filename}`);
712
+ fetchExports();
713
+ } else {
714
+ alert(`❌ Failed: ${data.error}`);
715
+ }
716
+ });
717
+ }
718
+
719
+ function startMeeting() {
720
+ const startBtn = document.getElementById("start-meeting");
721
+ const stopBtn = document.getElementById("stop-meeting");
722
+ const statusEl = document.getElementById("meeting-status");
723
+ const speakerEl = document.getElementById("speaker-label");
724
+ const timelineEl = document.getElementById("timeline");
725
+
726
+ meetingId = new Date().toISOString().replace(/[:.]/g, "-");
727
+ meetingBlob = null;
728
+
729
+ startBtn.disabled = true;
730
+ stopBtn.disabled = false;
731
+ statusEl.textContent = "Status: Preparing recording...";
732
+ statusEl.style.color = "red";
733
+ speakerEl.textContent = "Current speaker: —";
734
+ timelineEl.innerHTML = "<em>🎧 Listening...</em>";
735
+
736
+ showMicOverlay({
737
+ title: "🎙️ Meeting Mode",
738
+ message: "Capturing meeting audio... Meeting mode will continue until you stop it manually.",
739
+ countdownSeconds: 3,
740
+ onStop: (blob) => {
741
+ // This function will be called when stopMeeting is invoked
742
+ },
743
+ onStreamReady: (stream, stopOverlayRecording) => {
744
+ meetingMediaRecorder = new MediaRecorder(stream, { mimeType: "audio/webm;codecs=opus" });
745
+
746
+ meetingMediaRecorder.ondataavailable = (e) => {
747
+ if (e.data && e.data.size > 0) meetingBlob = e.data;
748
+ };
749
+
750
+ meetingMediaRecorder.onstop = async () => {
751
+ stopBtn.disabled = true;
752
+ startBtn.disabled = false;
753
+ statusEl.textContent = "Status: Recording stopped.";
754
+ statusEl.style.color = "";
755
+
756
+ if (meetingBlob) {
757
+ statusEl.textContent = "⏳ Uploading and processing...";
758
+ const formData = new FormData();
759
+ formData.append("file", meetingBlob, `${meetingId}.webm`);
760
+ formData.append("meeting_id", meetingId);
761
+
762
+ try {
763
+ await fetch("/api/save-chunk", { method: "POST", body: formData });
764
+ fetchMeetings();
765
+ statusEl.textContent = "✅ Meeting saved.";
766
+ } catch (err) {
767
+ console.error("❌ Failed to save meeting:", err);
768
+ statusEl.textContent = "❌ Failed to save meeting.";
769
+ } finally {
770
+ closeMicOverlay();
771
+ }
772
+ }
773
+ };
774
+
775
+ meetingMediaRecorder.start();
776
+ statusEl.textContent = "🔴 Recording meeting...";
777
+ },
778
+ });
779
+ }
780
+
781
+ function stopMeeting() {
782
+ if (meetingMediaRecorder?.state === "recording") {
783
+ meetingMediaRecorder.stop();
784
+ }
785
+ }
786
+
787
+ async function generateSummaryFor(meetingId) {
788
+ const timelineEl = document.getElementById("timeline");
789
+ timelineEl.innerHTML = "<strong>📄 Loading summary...</strong>";
790
+
791
+ try {
792
+ const res = await fetch(`/api/generate-summary/${meetingId}`);
793
+ const data = await res.json();
794
+
795
+ timelineEl.innerHTML = "<strong>📄 Meeting Summary:</strong>";
796
+
797
+ if (data.transcript) {
798
+ const pre = document.createElement("pre");
799
+ pre.textContent = data.transcript;
800
+ pre.style.marginBottom = "1rem";
801
+ timelineEl.appendChild(pre);
802
+ }
803
+
804
+ if (Array.isArray(data.segments)) {
805
+ data.segments.forEach((seg) => {
806
+ const div = document.createElement("div");
807
+ div.className = "segment-block";
808
+ div.innerHTML = `
809
+ <div class="segment-meta">
810
+ <span class="segment-time">[${formatTime(seg.start)}–${formatTime(seg.end)}]</span>
811
+ <span class="segment-speaker">${seg.speaker}</span>
812
+ <span class="segment-score">(${(seg.score ?? 0).toFixed(2)})</span>
813
+ </div>
814
+ <blockquote class="segment-text">${seg.text}</blockquote>
815
+ `;
816
+
817
+ const feedbackBtn = document.createElement("button");
818
+ feedbackBtn.textContent = "✏️ Correct Speaker";
819
+ feedbackBtn.style.marginLeft = "10px";
820
+
821
+ feedbackBtn.onclick = () => {
822
+ const wrapper = document.createElement("div");
823
+ wrapper.style.marginTop = "0.5rem";
824
+
825
+ const label = document.createElement("label");
826
+ label.textContent = "Correct speaker: ";
827
+ label.style.marginRight = "0.5rem";
828
+
829
+ const input = document.createElement("input");
830
+ input.setAttribute("list", "speaker-options");
831
+ input.placeholder = "e.g. Lara or new...";
832
+ input.style.width = "200px";
833
+
834
+ const dataList = document.createElement("datalist");
835
+ dataList.id = "speaker-options";
836
+ knownSpeakers.forEach((name) => {
837
+ const opt = document.createElement("option");
838
+ opt.value = name;
839
+ dataList.appendChild(opt);
840
+ });
841
+
842
+ const confirmBtn = document.createElement("button");
843
+ confirmBtn.textContent = "✅ Confirm";
844
+ confirmBtn.style.marginLeft = "0.5rem";
845
+
846
+ confirmBtn.onclick = async () => {
847
+ const correctedName = input.value.trim();
848
+ if (!correctedName) return alert("Please enter a name.");
849
+
850
+ feedbackBtn.disabled = true;
851
+ confirmBtn.disabled = true;
852
+ confirmBtn.textContent = "⏳ Correcting...";
853
+
854
+ try {
855
+ const payload = {
856
+ old_speaker: seg.speaker,
857
+ correct_speaker: correctedName,
858
+ filename: seg.filename || "" // Ensure filename is returned by backend
859
+ };
860
+
861
+ const res = await fetch("/api/correct-segment", {
862
+ method: "POST",
863
+ headers: { "Content-Type": "application/json" },
864
+ body: JSON.stringify(payload),
865
+ });
866
+
867
+ if (res.ok) {
868
+ div.querySelector(".segment-speaker").textContent = correctedName;
869
+ alert(`✅ Reclassified to ${correctedName}`);
870
+ fetchSpeakers();
871
+ } else {
872
+ alert("❌ Correction failed.");
873
+ }
874
+ } catch (err) {
875
+ alert(`❌ Error: ${err}`);
876
+ } finally {
877
+ wrapper.remove();
878
+ }
879
+ };
880
+
881
+ wrapper.appendChild(label);
882
+ wrapper.appendChild(input);
883
+ wrapper.appendChild(dataList);
884
+ wrapper.appendChild(confirmBtn);
885
+ div.appendChild(wrapper);
886
+ };
887
+
888
+ div.appendChild(feedbackBtn);
889
+ timelineEl.appendChild(div);
890
+ });
891
+ } else {
892
+ timelineEl.innerHTML += "<p><em>No segments found.</em></p>";
893
+ }
894
+ } catch (err) {
895
+ console.error("❌ Failed to generate summary:", err);
896
+ timelineEl.innerHTML = "<strong>❌ Failed to load summary.</strong>";
897
+ }
898
+ }
899
+
900
+ function deleteMeeting(meetingId) {
901
+ if (!confirm(`Delete meeting: ${meetingId}?`)) return;
902
+
903
+ fetch(`/api/delete-meeting/${meetingId}`, { method: "DELETE" })
904
+ .then((res) => res.json())
905
+ .then((data) => {
906
+ if (data.deleted) {
907
+ alert(`✅ Deleted ${meetingId}`);
908
+ fetchMeetings();
909
+ } else {
910
+ alert(`❌ Failed: ${data.error}`);
911
+ }
912
+ });
913
+ }
914
+
915
+ function fetchMeetings() {
916
+ fetch("/api/meetings")
917
+ .then(res => res.json())
918
+ .then(meetings => {
919
+ const list = document.getElementById("meeting-list");
920
+ if (!list) return; // <== ✅ prevent error
921
+ if (meetings.length === 0) {
922
+ list.innerHTML = "<em>No meetings found.</em>";
923
+ } else {
924
+ list.innerHTML = meetings.map(m => `<li>${m}</li>`).join("");
925
+ }
926
+ });
927
+ }
928
+
929
+ function fetchExports() {
930
+ fetch("/api/exports")
931
+ .then(res => res.json())
932
+ .then(exports => {
933
+ const list = document.getElementById("export-list");
934
+ if (!list) return; // <== ✅ prevent error
935
+ if (exports.length === 0) {
936
+ list.innerHTML = "<em>No exports found.</em>";
937
+ } else {
938
+ list.innerHTML = exports.map(e => `<li>${e}</li>`).join("");
939
+ }
940
+ });
941
+ }
942
+
943
+ function fetchRecordings() {
944
+ fetch("/api/recordings")
945
+ .then(res => res.json())
946
+ .then(data => {
947
+ const list = document.getElementById("recording-list");
948
+ if (!list) return; // <== ✅ prevent error
949
+ const html = Object.entries(data).map(([name, files]) => {
950
+ return `<li><strong>${name}</strong>: ${files.length} files</li>`;
951
+ }).join("");
952
+ list.innerHTML = html || "<em>No recordings found.</em>";
953
+ });
954
+ }