yasserrmd commited on
Commit
ff2faa2
·
verified ·
1 Parent(s): 9590982

Update static/conv.html

Browse files
Files changed (1) hide show
  1. static/conv.html +437 -327
static/conv.html CHANGED
@@ -442,361 +442,471 @@
442
  <!-- Bootstrap JS Bundle -->
443
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
444
  <script>
445
- // DOM Elements
446
- const listenBall = document.getElementById("listenBall");
447
- const statusMessage = document.getElementById("statusMessage");
448
- const audioPlayer = document.getElementById("audioPlayer");
449
- const conversationArea = document.getElementById("conversationArea");
450
- const emptyState = document.getElementById("emptyState");
451
- const statusBadge = document.getElementById("statusBadge");
452
-
453
- // Global variables
454
- let mediaRecorder;
455
- let audioChunks = [];
456
- let audioStream;
457
- let chatHistory = [];
458
- let isListening = false;
459
- let isAutoListening = false;
460
- let silenceDetectionInterval;
461
- let lastAudioLevel = 0;
462
- let currentUserGroup = null;
463
- let currentAssistantGroup = null;
464
-
465
- // Functions
466
- function updateStatus(message, icon = "bi-info-circle") {
467
- statusMessage.textContent = message;
468
- statusBadge.querySelector("i").className = `bi ${icon}`;
469
- }
470
-
471
- function addMessageToChat(content, sender) {
472
- // Hide empty state if it's visible
473
- if (!emptyState.classList.contains("d-none")) {
474
- emptyState.classList.add("d-none");
475
- }
476
-
477
- const now = new Date();
478
- const timeString = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
479
-
480
- // Create new message group or use existing one based on sender
481
- let messageGroup;
482
-
483
- if (sender === 'user') {
484
- if (!currentUserGroup || (currentAssistantGroup && currentAssistantGroup.classList.contains("assistant"))) {
485
- currentUserGroup = document.createElement("div");
486
- currentUserGroup.className = "message-group user";
487
-
488
- const avatar = document.createElement("div");
489
- avatar.className = "message-avatar";
490
- avatar.innerHTML = "<i class='bi bi-person'></i>";
491
- currentUserGroup.appendChild(avatar);
492
-
493
- conversationArea.appendChild(currentUserGroup);
494
- }
495
- messageGroup = currentUserGroup;
496
- currentAssistantGroup = null;
497
- } else {
498
- if (!currentAssistantGroup || (currentUserGroup && currentUserGroup.classList.contains("user"))) {
499
- currentAssistantGroup = document.createElement("div");
500
- currentAssistantGroup.className = "message-group assistant";
501
-
502
- const avatar = document.createElement("div");
503
- avatar.className = "message-avatar";
504
- avatar.innerHTML = "<i class='bi bi-robot'></i>";
505
- currentAssistantGroup.appendChild(avatar);
506
-
507
- conversationArea.appendChild(currentAssistantGroup);
508
- }
509
- messageGroup = currentAssistantGroup;
510
- currentUserGroup = null;
511
- }
512
-
513
- // Create message element
514
- const messageDiv = document.createElement("div");
515
- messageDiv.className = `message ${sender}`;
516
- messageDiv.textContent = content;
517
-
518
- const timestamp = document.createElement("div");
519
- timestamp.className = "time-stamp";
520
- timestamp.textContent = timeString;
521
 
522
- messageGroup.appendChild(messageDiv);
523
- messageGroup.appendChild(timestamp);
 
 
524
 
525
- // Scroll to bottom
526
- conversationArea.scrollTop = conversationArea.scrollHeight;
 
 
 
 
 
 
527
 
528
- // Add to chat history
529
- chatHistory.push({
530
- role: sender === 'user' ? 'user' : 'assistant',
531
- content: content
532
- });
533
- }
534
-
535
- function startAnalyzingAudioLevels() {
536
- if (!audioStream) return;
537
 
538
- const audioContext = new AudioContext();
539
- const source = audioContext.createMediaStreamSource(audioStream);
540
- const analyzer = audioContext.createAnalyser();
541
- analyzer.fftSize = 256;
542
- source.connect(analyzer);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
543
 
544
- const bufferLength = analyzer.frequencyBinCount;
545
- const dataArray = new Uint8Array(bufferLength);
546
 
547
- let silenceCounter = 0;
548
- const SILENCE_THRESHOLD = 10;
549
- const MAX_SILENCE_COUNT = 20; // About 2 seconds of silence
 
 
 
 
550
 
551
- clearInterval(silenceDetectionInterval);
552
- silenceDetectionInterval = setInterval(() => {
553
- analyzer.getByteFrequencyData(dataArray);
 
554
 
555
- // Calculate average audio level
556
- let sum = 0;
557
- for (let i = 0; i < bufferLength; i++) {
558
- sum += dataArray[i];
559
  }
560
- const avg = sum / bufferLength;
561
- lastAudioLevel = avg;
562
-
563
- // Detect significant sound and silence
564
- if (avg > SILENCE_THRESHOLD) {
565
- silenceCounter = 0;
566
- if (!isListening && isAutoListening) {
567
- startListening();
568
- }
569
- } else {
570
- silenceCounter++;
571
- if (silenceCounter >= MAX_SILENCE_COUNT && isListening) {
572
- stopListening();
573
- }
574
- }
575
- }, 100);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
576
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
577
 
578
- async function startListening() {
579
- if (isListening) return;
 
 
 
 
 
 
 
 
 
580
 
581
  try {
582
- // Stop any existing stream tracks
583
- if (audioStream) {
584
- audioStream.getTracks().forEach(track => track.stop());
585
- }
586
 
587
- audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
588
- mediaRecorder = new MediaRecorder(audioStream, { mimeType: "audio/webm" });
 
 
589
 
590
- mediaRecorder.ondataavailable = event => audioChunks.push(event.data);
 
 
 
 
591
 
592
- mediaRecorder.onstop = async () => {
593
- if (audioChunks.length === 0) {
594
- updateState("idle");
595
- return;
596
- }
597
 
598
- updateState("processing");
 
 
599
 
600
- try {
601
- const audioBlob = new Blob(audioChunks, { type: "audio/webm" });
602
- const wavBlob = await convertWebMToWav(audioBlob);
603
-
604
- // Create form data with the audio and chat history
605
- const formData = new FormData();
606
- formData.append("file", wavBlob, "recording.wav");
607
- formData.append("chat_history", JSON.stringify(chatHistory));
608
-
609
- // Send to the continuous-chat endpoint using root-relative path
610
- const response = await fetch("/continuous-chat/", {
611
- method: "POST",
612
- body: formData
613
- });
614
-
615
- if (response.ok) {
616
- const userMessage = response.headers.get("X-User-Message") || "No user message";
617
- const llmResponse = response.headers.get("X-LLM-Response") || "No response";
618
-
619
- // Add messages to chat
620
- addMessageToChat(userMessage, 'user');
621
- addMessageToChat(llmResponse, 'assistant');
622
-
623
- // Get audio response and play it
624
- const audioData = await response.blob();
625
- audioPlayer.src = URL.createObjectURL(audioData);
626
- audioPlayer.play();
627
-
628
- updateState("idle");
629
- updateStatus("Ready for next sound input", "bi-broadcast");
630
-
631
- // If in auto mode, start listening again
632
- if (isAutoListening) {
633
- setTimeout(() => {
634
- startAnalyzingAudioLevels();
635
- }, 1000);
636
- }
637
- } else {
638
- updateState("idle");
639
- updateStatus("Error processing audio", "bi-exclamation-triangle");
640
- }
641
- } catch (error) {
642
- console.error("Error:", error);
643
- updateState("idle");
644
- updateStatus("Error processing audio", "bi-exclamation-triangle");
645
- }
646
- };
647
-
648
- audioChunks = [];
649
- mediaRecorder.start();
650
-
651
- updateState("listening");
652
- updateStatus("Listening...", "bi-ear");
653
-
654
- // Set max recording duration (8 seconds)
655
- setTimeout(() => {
656
- if (mediaRecorder && mediaRecorder.state === "recording") {
657
- stopListening();
658
- }
659
- }, 8000);
660
-
661
- isListening = true;
662
  } catch (error) {
663
- console.error("Error accessing microphone:", error);
664
- updateStatus("Microphone access denied", "bi-mic-mute");
665
  updateState("idle");
 
666
  }
667
- }
668
-
669
- function stopListening() {
670
- if (!isListening) return;
671
-
672
- if (mediaRecorder && mediaRecorder.state === "recording") {
673
- mediaRecorder.stop();
674
- }
675
-
676
- isListening = false;
677
- }
678
-
679
- function updateState(state) {
680
- listenBall.classList.remove("listening", "processing");
681
-
682
- if (state === "listening") {
683
- listenBall.classList.add("listening");
684
- listenBall.innerHTML = `
685
- <div class="sound-wave"></div>
686
- <div class="sound-wave" style="animation-delay: 0.5s"></div>
687
- <div class="sound-wave" style="animation-delay: 1s"></div>
688
- <i class="bi bi-soundwave"></i>
689
- `;
690
- } else if (state === "processing") {
691
- listenBall.classList.add("processing");
692
- listenBall.innerHTML = `<i class="bi bi-arrow-repeat"></i>`;
693
- } else {
694
- listenBall.innerHTML = `<i class="bi bi-soundwave"></i>`;
695
- }
696
- }
697
-
698
- function toggleContinuousListening() {
699
- isAutoListening = !isAutoListening;
700
 
701
- if (isAutoListening) {
702
- updateStatus("Auto-listening mode active", "bi-broadcast");
703
- startAnalyzingAudioLevels();
704
- } else {
705
- updateStatus("Tap to listen", "bi-info-circle");
706
- clearInterval(silenceDetectionInterval);
707
- }
708
- }
709
-
710
- async function convertWebMToWav(blob) {
711
- return new Promise((resolve, reject) => {
712
- try {
713
- const reader = new FileReader();
714
- reader.onload = function () {
715
- const audioContext = new AudioContext();
716
- audioContext.decodeAudioData(reader.result)
717
- .then(buffer => {
718
- const wavBuffer = audioBufferToWav(buffer);
719
- resolve(new Blob([wavBuffer], { type: "audio/wav" }));
720
- })
721
- .catch(error => {
722
- console.error("Error decoding audio data:", error);
723
- reject(error);
724
- });
725
- };
726
- reader.readAsArrayBuffer(blob);
727
- } catch (error) {
728
- console.error("Error in convertWebMToWav:", error);
729
- reject(error);
730
- }
731
- });
732
- }
733
-
734
- function audioBufferToWav(buffer) {
735
- let numOfChan = buffer.numberOfChannels,
736
- length = buffer.length * numOfChan * 2 + 44,
737
- bufferArray = new ArrayBuffer(length),
738
- view = new DataView(bufferArray),
739
- channels = [],
740
- sampleRate = buffer.sampleRate,
741
- offset = 0,
742
- pos = 0;
743
- setUint32(0x46464952); // "RIFF"
744
- setUint32(length - 8);
745
- setUint32(0x45564157); // "WAVE"
746
- setUint32(0x20746d66); // "fmt " chunk
747
- setUint32(16); // length = 16
748
- setUint16(1); // PCM (uncompressed)
749
- setUint16(numOfChan);
750
- setUint32(sampleRate);
751
- setUint32(sampleRate * 2 * numOfChan);
752
- setUint16(numOfChan * 2);
753
- setUint16(16); // bits per sample
754
- setUint32(0x61746164); // "data" chunk
755
- setUint32(length - pos - 4);
756
- for (let i = 0; i < buffer.numberOfChannels; i++)
757
- channels.push(buffer.getChannelData(i));
758
- while (pos < length) {
759
- for (let i = 0; i < numOfChan; i++) {
760
- let sample = Math.max(-1, Math.min(1, channels[i][offset]));
761
- sample = sample < 0 ? sample * 0x8000 : sample * 0x7FFF;
762
- setUint16(sample);
763
  }
764
- offset++;
765
- }
766
- function setUint16(data) {
767
- view.setUint16(pos, data, true);
768
- pos += 2;
769
- }
770
- function setUint32(data) {
771
- view.setUint32(pos, data, true);
772
- pos += 4;
773
- }
774
- return bufferArray;
775
- }
776
 
777
- // Event Listeners
778
- listenBall.addEventListener("click", () => {
779
- if (isListening) {
780
- stopListening();
781
- } else if (isAutoListening) {
782
- toggleContinuousListening();
783
- } else {
784
- startListening();
785
- }
786
- });
787
 
788
- listenBall.addEventListener("dblclick", toggleContinuousListening);
 
789
 
790
- audioPlayer.addEventListener("ended", () => {
791
- if (isAutoListening) {
792
- setTimeout(() => {
793
- startAnalyzingAudioLevels();
794
- }, 500);
795
  }
796
- });
797
-
798
- // Initialize
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
799
  updateStatus("Tap to listen", "bi-info-circle");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
800
  </script>
801
  </body>
802
  </html>
 
442
  <!-- Bootstrap JS Bundle -->
443
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
444
  <script>
445
+ // DOM Elements
446
+ const listenBall = document.getElementById("listenBall");
447
+ const statusMessage = document.getElementById("statusMessage");
448
+ const audioPlayer = document.getElementById("audioPlayer");
449
+ const conversationArea = document.getElementById("conversationArea");
450
+ const emptyState = document.getElementById("emptyState");
451
+ const statusBadge = document.getElementById("statusBadge");
452
+
453
+ // Global variables
454
+ let mediaRecorder;
455
+ let audioChunks = [];
456
+ let audioStream;
457
+ let chatHistory = [];
458
+ let isListening = false;
459
+ let isAutoListening = false;
460
+ let silenceDetectionInterval;
461
+ let activityDetectionInterval;
462
+ let lastAudioLevel = 0;
463
+ let silenceCounter = 0;
464
+ let activityCounter = 0;
465
+ let currentUserGroup = null;
466
+ let currentAssistantGroup = null;
467
+ let audioContext;
468
+ let analyzer;
469
+ let isProcessing = false;
470
+
471
+ // Constants
472
+ const SILENCE_THRESHOLD = 15;
473
+ const ACTIVITY_THRESHOLD = 20;
474
+ const MIN_ACTIVITY_DURATION = 5; // Minimum counts of activity before recording
475
+ const MAX_SILENCE_DURATION = 15; // Maximum counts of silence before stopping
476
+ const MAX_RECORDING_DURATION = 8000; // Maximum recording duration in ms
477
+ const COOLDOWN_PERIOD = 1000; // Cooldown between recordings
478
+
479
+ // Functions
480
+ function updateStatus(message, icon = "bi-info-circle") {
481
+ statusMessage.textContent = message;
482
+ statusBadge.querySelector("i").className = `bi ${icon}`;
483
+ }
484
+
485
+ function addMessageToChat(content, sender) {
486
+ // Hide empty state if it's visible
487
+ if (!emptyState.classList.contains("d-none")) {
488
+ emptyState.classList.add("d-none");
489
+ }
490
+
491
+ const now = new Date();
492
+ const timeString = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
493
+
494
+ // Create new message group or use existing one based on sender
495
+ let messageGroup;
496
+
497
+ if (sender === 'user') {
498
+ if (!currentUserGroup || (currentAssistantGroup && currentAssistantGroup.classList.contains("assistant"))) {
499
+ currentUserGroup = document.createElement("div");
500
+ currentUserGroup.className = "message-group user";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
 
502
+ const avatar = document.createElement("div");
503
+ avatar.className = "message-avatar";
504
+ avatar.innerHTML = "<i class='bi bi-person'></i>";
505
+ currentUserGroup.appendChild(avatar);
506
 
507
+ conversationArea.appendChild(currentUserGroup);
508
+ }
509
+ messageGroup = currentUserGroup;
510
+ currentAssistantGroup = null;
511
+ } else {
512
+ if (!currentAssistantGroup || (currentUserGroup && currentUserGroup.classList.contains("user"))) {
513
+ currentAssistantGroup = document.createElement("div");
514
+ currentAssistantGroup.className = "message-group assistant";
515
 
516
+ const avatar = document.createElement("div");
517
+ avatar.className = "message-avatar";
518
+ avatar.innerHTML = "<i class='bi bi-robot'></i>";
519
+ currentAssistantGroup.appendChild(avatar);
 
 
 
 
 
520
 
521
+ conversationArea.appendChild(currentAssistantGroup);
522
+ }
523
+ messageGroup = currentAssistantGroup;
524
+ currentUserGroup = null;
525
+ }
526
+
527
+ // Create message element
528
+ const messageDiv = document.createElement("div");
529
+ messageDiv.className = `message ${sender}`;
530
+ messageDiv.textContent = content;
531
+
532
+ const timestamp = document.createElement("div");
533
+ timestamp.className = "time-stamp";
534
+ timestamp.textContent = timeString;
535
+
536
+ messageGroup.appendChild(messageDiv);
537
+ messageGroup.appendChild(timestamp);
538
+
539
+ // Scroll to bottom
540
+ conversationArea.scrollTop = conversationArea.scrollHeight;
541
+
542
+ // Add to chat history
543
+ chatHistory.push({
544
+ role: sender === 'user' ? 'user' : 'assistant',
545
+ content: content
546
+ });
547
+ }
548
+
549
+ async function setupAudioAnalysis() {
550
+ if (audioContext) {
551
+ audioContext.close();
552
+ }
553
+
554
+ audioContext = new AudioContext();
555
+ const source = audioContext.createMediaStreamSource(audioStream);
556
+ analyzer = audioContext.createAnalyser();
557
+ analyzer.fftSize = 256;
558
+ source.connect(analyzer);
559
+
560
+ const bufferLength = analyzer.frequencyBinCount;
561
+ const dataArray = new Uint8Array(bufferLength);
562
+
563
+ return { analyzer, dataArray, bufferLength };
564
+ }
565
+
566
+ function startContinuousListening() {
567
+ if (!audioStream) return;
568
+
569
+ // Set up audio analysis
570
+ setupAudioAnalysis().then(({ analyzer, dataArray, bufferLength }) => {
571
+ // Start monitoring audio levels
572
+ clearInterval(activityDetectionInterval);
573
+ activityDetectionInterval = setInterval(() => {
574
+ if (isProcessing) return;
575
 
576
+ analyzer.getByteFrequencyData(dataArray);
 
577
 
578
+ // Calculate average audio level
579
+ let sum = 0;
580
+ for (let i = 0; i < bufferLength; i++) {
581
+ sum += dataArray[i];
582
+ }
583
+ const avg = sum / bufferLength;
584
+ lastAudioLevel = avg;
585
 
586
+ // Detect significant sound
587
+ if (avg > ACTIVITY_THRESHOLD) {
588
+ activityCounter++;
589
+ silenceCounter = 0;
590
 
591
+ // If we have enough continuous activity and not already listening, start recording
592
+ if (activityCounter >= MIN_ACTIVITY_DURATION && !isListening && !isProcessing) {
593
+ startRecording();
 
594
  }
595
+ } else {
596
+ activityCounter = 0;
597
+ }
598
+ }, 100);
599
+ });
600
+ }
601
+
602
+ function monitorSilenceDuringRecording() {
603
+ if (!audioStream || !isListening) return;
604
+
605
+ clearInterval(silenceDetectionInterval);
606
+ silenceDetectionInterval = setInterval(() => {
607
+ if (!isListening) {
608
+ clearInterval(silenceDetectionInterval);
609
+ return;
610
+ }
611
+
612
+ analyzer.getByteFrequencyData(new Uint8Array(analyzer.frequencyBinCount));
613
+
614
+ // Calculate average audio level
615
+ let sum = 0;
616
+ for (let i = 0; i < analyzer.frequencyBinCount; i++) {
617
+ sum += dataArray[i];
618
+ }
619
+ const avg = sum / analyzer.frequencyBinCount;
620
+
621
+ // If silent, increment counter
622
+ if (avg < SILENCE_THRESHOLD) {
623
+ silenceCounter++;
624
+ if (silenceCounter >= MAX_SILENCE_DURATION) {
625
+ stopRecording();
626
+ }
627
+ } else {
628
+ silenceCounter = 0;
629
  }
630
+ }, 100);
631
+ }
632
+
633
+ async function startRecording() {
634
+ if (isListening || isProcessing) return;
635
+
636
+ try {
637
+ // Reset counters
638
+ silenceCounter = 0;
639
+
640
+ // Start recording
641
+ mediaRecorder = new MediaRecorder(audioStream, { mimeType: "audio/webm" });
642
+ audioChunks = [];
643
 
644
+ mediaRecorder.ondataavailable = event => audioChunks.push(event.data);
645
+
646
+ mediaRecorder.onstop = async () => {
647
+ if (audioChunks.length === 0) {
648
+ updateState("idle");
649
+ isProcessing = false;
650
+ return;
651
+ }
652
+
653
+ isProcessing = true;
654
+ updateState("processing");
655
 
656
  try {
657
+ const audioBlob = new Blob(audioChunks, { type: "audio/webm" });
658
+ const wavBlob = await convertWebMToWav(audioBlob);
 
 
659
 
660
+ // Create form data with the audio and chat history
661
+ const formData = new FormData();
662
+ formData.append("file", wavBlob, "recording.wav");
663
+ formData.append("chat_history", JSON.stringify(chatHistory));
664
 
665
+ // Send to the continuous-chat endpoint using root-relative path
666
+ const response = await fetch("/continuous-chat/", {
667
+ method: "POST",
668
+ body: formData
669
+ });
670
 
671
+ if (response.ok) {
672
+ const userMessage = response.headers.get("X-User-Message") || "No user message";
673
+ const llmResponse = response.headers.get("X-LLM-Response") || "No response";
 
 
674
 
675
+ // Add messages to chat
676
+ addMessageToChat(userMessage, 'user');
677
+ addMessageToChat(llmResponse, 'assistant');
678
 
679
+ // Get audio response and play it
680
+ const audioData = await response.blob();
681
+ audioPlayer.src = URL.createObjectURL(audioData);
682
+ audioPlayer.play();
683
+
684
+ updateState("idle");
685
+ updateStatus("Listening for sound", "bi-broadcast");
686
+ } else {
687
+ updateState("idle");
688
+ updateStatus("Error processing audio", "bi-exclamation-triangle");
689
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
690
  } catch (error) {
691
+ console.error("Error:", error);
 
692
  updateState("idle");
693
+ updateStatus("Error processing audio", "bi-exclamation-triangle");
694
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
695
 
696
+ // Set a cooldown before allowing the next recording
697
+ setTimeout(() => {
698
+ isProcessing = false;
699
+ if (isAutoListening) {
700
+ updateStatus("Listening for sound", "bi-broadcast");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
701
  }
702
+ }, COOLDOWN_PERIOD);
703
+ };
 
 
 
 
 
 
 
 
 
 
704
 
705
+ mediaRecorder.start();
706
+ isListening = true;
707
+ updateState("listening");
708
+ updateStatus("Recording...", "bi-ear");
 
 
 
 
 
 
709
 
710
+ // Monitor for silence during recording
711
+ monitorSilenceDuringRecording();
712
 
713
+ // Set max recording duration
714
+ setTimeout(() => {
715
+ if (mediaRecorder && mediaRecorder.state === "recording") {
716
+ stopRecording();
 
717
  }
718
+ }, MAX_RECORDING_DURATION);
719
+
720
+ } catch (error) {
721
+ console.error("Error starting recording:", error);
722
+ updateState("idle");
723
+ updateStatus("Recording error", "bi-exclamation-triangle");
724
+ isListening = false;
725
+ isProcessing = false;
726
+ }
727
+ }
728
+
729
+ function stopRecording() {
730
+ if (!isListening) return;
731
+
732
+ clearInterval(silenceDetectionInterval);
733
+
734
+ if (mediaRecorder && mediaRecorder.state === "recording") {
735
+ mediaRecorder.stop();
736
+ }
737
+
738
+ isListening = false;
739
+ updateStatus("Processing...", "bi-arrow-repeat");
740
+ }
741
+
742
+ function updateState(state) {
743
+ listenBall.classList.remove("listening", "processing");
744
+
745
+ if (state === "listening") {
746
+ listenBall.classList.add("listening");
747
+ listenBall.innerHTML = `
748
+ <div class="sound-wave"></div>
749
+ <div class="sound-wave" style="animation-delay: 0.5s"></div>
750
+ <div class="sound-wave" style="animation-delay: 1s"></div>
751
+ <i class="bi bi-soundwave"></i>
752
+ `;
753
+ } else if (state === "processing") {
754
+ listenBall.classList.add("processing");
755
+ listenBall.innerHTML = `<i class="bi bi-arrow-repeat"></i>`;
756
+ } else {
757
+ listenBall.innerHTML = `<i class="bi bi-soundwave"></i>`;
758
+ }
759
+ }
760
+
761
+ async function toggleContinuousListening() {
762
+ isAutoListening = !isAutoListening;
763
+
764
+ if (isAutoListening) {
765
+ try {
766
+ // Request microphone access if we don't have it
767
+ if (!audioStream) {
768
+ audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
769
+ }
770
+
771
+ updateStatus("Auto-listening active", "bi-broadcast");
772
+ startContinuousListening();
773
+ } catch (error) {
774
+ console.error("Error accessing microphone:", error);
775
+ updateStatus("Microphone access denied", "bi-mic-mute");
776
+ isAutoListening = false;
777
+ }
778
+ } else {
779
+ // Stop continuous listening
780
+ clearInterval(activityDetectionInterval);
781
+ clearInterval(silenceDetectionInterval);
782
  updateStatus("Tap to listen", "bi-info-circle");
783
+
784
+ // If currently recording, stop it
785
+ if (isListening) {
786
+ stopRecording();
787
+ }
788
+ }
789
+ }
790
+
791
+ async function manualListening() {
792
+ if (isListening || isProcessing) return;
793
+
794
+ try {
795
+ // Request microphone access if we don't have it
796
+ if (!audioStream) {
797
+ audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
798
+ await setupAudioAnalysis();
799
+ }
800
+
801
+ startRecording();
802
+ } catch (error) {
803
+ console.error("Error accessing microphone:", error);
804
+ updateStatus("Microphone access denied", "bi-mic-mute");
805
+ }
806
+ }
807
+
808
+ async function convertWebMToWav(blob) {
809
+ return new Promise((resolve, reject) => {
810
+ try {
811
+ const reader = new FileReader();
812
+ reader.onload = function () {
813
+ const audioContext = new AudioContext();
814
+ audioContext.decodeAudioData(reader.result)
815
+ .then(buffer => {
816
+ const wavBuffer = audioBufferToWav(buffer);
817
+ resolve(new Blob([wavBuffer], { type: "audio/wav" }));
818
+ })
819
+ .catch(error => {
820
+ console.error("Error decoding audio data:", error);
821
+ reject(error);
822
+ });
823
+ };
824
+ reader.readAsArrayBuffer(blob);
825
+ } catch (error) {
826
+ console.error("Error in convertWebMToWav:", error);
827
+ reject(error);
828
+ }
829
+ });
830
+ }
831
+
832
+ function audioBufferToWav(buffer) {
833
+ let numOfChan = buffer.numberOfChannels,
834
+ length = buffer.length * numOfChan * 2 + 44,
835
+ bufferArray = new ArrayBuffer(length),
836
+ view = new DataView(bufferArray),
837
+ channels = [],
838
+ sampleRate = buffer.sampleRate,
839
+ offset = 0,
840
+ pos = 0;
841
+ setUint32(0x46464952); // "RIFF"
842
+ setUint32(length - 8);
843
+ setUint32(0x45564157); // "WAVE"
844
+ setUint32(0x20746d66); // "fmt " chunk
845
+ setUint32(16); // length = 16
846
+ setUint16(1); // PCM (uncompressed)
847
+ setUint16(numOfChan);
848
+ setUint32(sampleRate);
849
+ setUint32(sampleRate * 2 * numOfChan);
850
+ setUint16(numOfChan * 2);
851
+ setUint16(16); // bits per sample
852
+ setUint32(0x61746164); // "data" chunk
853
+ setUint32(length - pos - 4);
854
+ for (let i = 0; i < buffer.numberOfChannels; i++)
855
+ channels.push(buffer.getChannelData(i));
856
+ while (pos < length) {
857
+ for (let i = 0; i < numOfChan; i++) {
858
+ let sample = Math.max(-1, Math.min(1, channels[i][offset]));
859
+ sample = sample < 0 ? sample * 0x8000 : sample * 0x7FFF;
860
+ setUint16(sample);
861
+ }
862
+ offset++;
863
+ }
864
+ function setUint16(data) {
865
+ view.setUint16(pos, data, true);
866
+ pos += 2;
867
+ }
868
+ function setUint32(data) {
869
+ view.setUint32(pos, data, true);
870
+ pos += 4;
871
+ }
872
+ return bufferArray;
873
+ }
874
+
875
+ // Event Listeners
876
+ listenBall.addEventListener("click", () => {
877
+ if (isAutoListening) {
878
+ toggleContinuousListening(); // Turn off auto mode
879
+ } else {
880
+ if (isListening) {
881
+ stopRecording(); // Stop manual recording
882
+ } else {
883
+ manualListening(); // Start manual recording
884
+ }
885
+ }
886
+ });
887
+
888
+ listenBall.addEventListener("dblclick", toggleContinuousListening);
889
+
890
+ audioPlayer.addEventListener("ended", () => {
891
+ if (isAutoListening && !isProcessing) {
892
+ updateStatus("Listening for sound", "bi-broadcast");
893
+ }
894
+ });
895
+
896
+ // Initialize
897
+ updateStatus("Tap to listen, double-tap for auto mode", "bi-info-circle");
898
+
899
+ // Cleanup function for page unload
900
+ window.addEventListener('beforeunload', () => {
901
+ if (audioStream) {
902
+ audioStream.getTracks().forEach(track => track.stop());
903
+ }
904
+ if (audioContext) {
905
+ audioContext.close();
906
+ }
907
+ clearInterval(silenceDetectionInterval);
908
+ clearInterval(activityDetectionInterval);
909
+ });
910
  </script>
911
  </body>
912
  </html>