| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Data-over-Sound Interface</title> |
| |
| <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> |
| |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css"> |
| <style> |
| body { |
| background-color: #f8f9fa; |
| padding-top: 30px; |
| } |
| .chat-container { |
| max-width: 700px; |
| margin: 0 auto; |
| background-color: white; |
| border-radius: 12px; |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); |
| padding: 25px; |
| } |
| .btn-record { |
| background-color: #ff4b4b; |
| border-color: #ff4b4b; |
| } |
| .btn-record:hover { |
| background-color: #e43c3c; |
| border-color: #e43c3c; |
| } |
| .btn-record.recording { |
| animation: pulse 1.5s infinite; |
| } |
| .btn-generate { |
| background-color: #4c6ef5; |
| border-color: #4c6ef5; |
| } |
| .btn-generate:hover { |
| background-color: #3b5bdb; |
| border-color: #3b5bdb; |
| } |
| .icon-spacing { |
| margin-right: 8px; |
| } |
| .control-label { |
| font-size: 0.9rem; |
| color: #6c757d; |
| margin-bottom: 8px; |
| } |
| .audio-container { |
| background-color: #f1f3f5; |
| border-radius: 8px; |
| padding: 15px; |
| margin-top: 20px; |
| } |
| audio { |
| width: 100%; |
| } |
| .status-indicator { |
| font-size: 0.9rem; |
| margin-top: 10px; |
| height: 24px; |
| } |
| .response-container { |
| background-color: #f8f9fa; |
| border-radius: 8px; |
| padding: 15px; |
| margin-top: 20px; |
| border: 1px solid #dee2e6; |
| } |
| .header-info { |
| font-family: monospace; |
| padding: 10px; |
| background-color: #e9ecef; |
| border-radius: 6px; |
| margin-bottom: 15px; |
| font-size: 0.9rem; |
| } |
| .text-input { |
| margin-top: 15px; |
| } |
| @keyframes pulse { |
| 0% { transform: scale(1); } |
| 50% { transform: scale(1.05); } |
| 100% { transform: scale(1); } |
| } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <div class="chat-container"> |
| <h1 class="text-center mb-4"> |
| <i class="bi bi-soundwave text-primary"></i> Data-over-Sound |
| </h1> |
| |
| <div class="text-input mb-4"> |
| <label for="messageInput" class="form-label">Message to Encode:</label> |
| <textarea id="messageInput" class="form-control" rows="3" placeholder="Enter text to encode as sound..."></textarea> |
| </div> |
| |
| <div class="row g-4"> |
| <div class="col-md-6"> |
| <div class="d-grid"> |
| <p class="control-label text-center">Listen for Data</p> |
| <button id="recordButton" class="btn btn-record btn-lg text-white"> |
| <i class="bi bi-mic-fill icon-spacing"></i> Start Listening |
| </button> |
| </div> |
| </div> |
| <div class="col-md-6"> |
| <div class="d-grid"> |
| <p class="control-label text-center">Transmit Data</p> |
| <button id="generateButton" class="btn btn-generate btn-lg text-white"> |
| <i class="bi bi-broadcast icon-spacing"></i> Transmit |
| </button> |
| </div> |
| </div> |
| </div> |
| |
| <div class="audio-container"> |
| <p class="control-label mb-2">Audio Control</p> |
| <audio id="audioPlayer" controls></audio> |
| <div id="statusMessage" class="status-indicator text-center text-secondary"></div> |
| </div> |
| |
| <div class="response-container"> |
| <h5><i class="bi bi-arrow-repeat icon-spacing"></i>Communication Headers</h5> |
| <div class="header-info"> |
| <div id="userMessageHeader">X-User-Message: <span class="text-primary">Waiting for data...</span></div> |
| <div id="llmResponseHeader">X-LLM-Response: <span class="text-success">Waiting for response...</span></div> |
| </div> |
| |
| <h5><i class="bi bi-reception-4 icon-spacing"></i>Data Received</h5> |
| <div id="receivedData" class="p-3 border rounded bg-white"> |
| No data received yet. |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> |
| <script> |
| let mediaRecorder; |
| let audioChunks = []; |
| let audioStream; |
| const recordButton = document.getElementById("recordButton"); |
| const generateButton = document.getElementById("generateButton"); |
| const statusMessage = document.getElementById("statusMessage"); |
| const messageInput = document.getElementById("messageInput"); |
| const userMessageHeader = document.getElementById("userMessageHeader"); |
| const llmResponseHeader = document.getElementById("llmResponseHeader"); |
| const receivedData = document.getElementById("receivedData"); |
| let recordingTimeout; |
| |
| recordButton.addEventListener("click", async () => { |
| if (!mediaRecorder || mediaRecorder.state === "inactive") { |
| try { |
| |
| if (audioStream) { |
| audioStream.getTracks().forEach(track => track.stop()); |
| } |
| |
| audioStream = await navigator.mediaDevices.getUserMedia({ audio: true }); |
| mediaRecorder = new MediaRecorder(audioStream, { mimeType: "audio/webm" }); |
| |
| mediaRecorder.ondataavailable = event => audioChunks.push(event.data); |
| |
| mediaRecorder.onstop = async () => { |
| statusMessage.textContent = "Processing audio data..."; |
| recordButton.innerHTML = '<i class="bi bi-mic-fill icon-spacing"></i> Start Listening'; |
| recordButton.classList.remove("recording"); |
| recordButton.classList.remove("btn-danger"); |
| recordButton.classList.add("btn-record"); |
| |
| try { |
| const audioBlob = new Blob(audioChunks, { type: "audio/webm" }); |
| const wavBlob = await convertWebMToWav(audioBlob); |
| |
| |
| const formData = new FormData(); |
| formData.append("file", wavBlob, "recording.wav"); |
| |
| const response = await fetch("/chat/", { |
| method: "POST", |
| body: formData |
| }); |
| |
| if (response.ok) { |
| |
| const userMessage = response.headers.get("X-User-Message") || "No user message"; |
| const llmResponse = response.headers.get("X-LLM-Response") || "No response"; |
| |
| |
| userMessageHeader.innerHTML = `X-User-Message: <span class="text-primary">${userMessage}</span>`; |
| llmResponseHeader.innerHTML = `X-LLM-Response: <span class="text-success">${llmResponse}</span>`; |
| |
| |
| receivedData.textContent = userMessage; |
| |
| |
| const audioData = await response.blob(); |
| document.getElementById("audioPlayer").src = URL.createObjectURL(audioData); |
| statusMessage.textContent = "Data decoded successfully!"; |
| } else { |
| statusMessage.textContent = "Error processing audio data. Please try again."; |
| } |
| } catch (error) { |
| console.error("Error:", error); |
| statusMessage.textContent = "Error processing audio data. Please try again."; |
| } |
| |
| |
| if (audioStream) { |
| audioStream.getTracks().forEach(track => track.stop()); |
| } |
| }; |
| |
| audioChunks = []; |
| mediaRecorder.start(); |
| |
| recordButton.innerHTML = '<i class="bi bi-stop-fill icon-spacing"></i> Listening...'; |
| recordButton.classList.add("recording"); |
| recordButton.classList.remove("btn-record"); |
| recordButton.classList.add("btn-danger"); |
| statusMessage.textContent = "Listening for data transmission..."; |
| |
| |
| recordingTimeout = setTimeout(() => { |
| if (mediaRecorder && mediaRecorder.state === "recording") { |
| mediaRecorder.stop(); |
| } |
| }, 5000); |
| } catch (error) { |
| console.error("Error accessing microphone:", error); |
| statusMessage.textContent = "Could not access microphone. Please check permissions."; |
| } |
| } else if (mediaRecorder.state === "recording") { |
| |
| clearTimeout(recordingTimeout); |
| mediaRecorder.stop(); |
| } |
| }); |
| |
| generateButton.addEventListener("click", async () => { |
| const text = messageInput.value.trim(); |
| if (text) { |
| statusMessage.textContent = "Encoding data to sound..."; |
| try { |
| const response = await fetch("/tts/", { |
| method: "POST", |
| headers: { |
| "Content-Type": "application/json" |
| }, |
| body: JSON.stringify({ text }) |
| }); |
| |
| if (response.ok) { |
| const audioData = await response.blob(); |
| document.getElementById("audioPlayer").src = URL.createObjectURL(audioData); |
| statusMessage.textContent = "Data encoded as sound. Ready to transmit!"; |
| |
| document.getElementById("audioPlayer").play(); |
| } else { |
| statusMessage.textContent = "Error encoding data. Please try again."; |
| } |
| } catch (error) { |
| console.error("Error:", error); |
| statusMessage.textContent = "Error encoding data. Please try again."; |
| } |
| } else { |
| statusMessage.textContent = "Please enter a message to transmit."; |
| } |
| }); |
| |
| async function convertWebMToWav(blob) { |
| return new Promise((resolve, reject) => { |
| try { |
| const reader = new FileReader(); |
| reader.onload = function () { |
| const audioContext = new AudioContext(); |
| audioContext.decodeAudioData(reader.result) |
| .then(buffer => { |
| const wavBuffer = audioBufferToWav(buffer); |
| resolve(new Blob([wavBuffer], { type: "audio/wav" })); |
| }) |
| .catch(error => { |
| console.error("Error decoding audio data:", error); |
| reject(error); |
| }); |
| }; |
| reader.readAsArrayBuffer(blob); |
| } catch (error) { |
| console.error("Error in convertWebMToWav:", error); |
| reject(error); |
| } |
| }); |
| } |
| |
| function audioBufferToWav(buffer) { |
| let numOfChan = buffer.numberOfChannels, |
| length = buffer.length * numOfChan * 2 + 44, |
| bufferArray = new ArrayBuffer(length), |
| view = new DataView(bufferArray), |
| channels = [], |
| sampleRate = buffer.sampleRate, |
| offset = 0, |
| pos = 0; |
| |
| setUint32(0x46464952); |
| setUint32(length - 8); |
| setUint32(0x45564157); |
| setUint32(0x20746d66); |
| setUint32(16); |
| setUint16(1); |
| setUint16(numOfChan); |
| setUint32(sampleRate); |
| setUint32(sampleRate * 2 * numOfChan); |
| setUint16(numOfChan * 2); |
| setUint16(16); |
| setUint32(0x61746164); |
| setUint32(length - pos - 4); |
| |
| for (let i = 0; i < buffer.numberOfChannels; i++) |
| channels.push(buffer.getChannelData(i)); |
| |
| while (pos < length) { |
| for (let i = 0; i < numOfChan; i++) { |
| let sample = Math.max(-1, Math.min(1, channels[i][offset])); |
| sample = sample < 0 ? sample * 0x8000 : sample * 0x7FFF; |
| setUint16(sample); |
| } |
| offset++; |
| } |
| |
| function setUint16(data) { |
| view.setUint16(pos, data, true); |
| pos += 2; |
| } |
| |
| function setUint32(data) { |
| view.setUint32(pos, data, true); |
| pos += 4; |
| } |
| |
| return bufferArray; |
| } |
| </script> |
| </body> |
| </html> |