Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Real-Time Voice Translator</title> | |
| <style> | |
| body { font-family: sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; margin: 0; background-color: #f0f0f0; } | |
| #controls { margin-bottom: 20px; } | |
| button { font-size: 1.2em; padding: 10px 20px; cursor: pointer; } | |
| #status { font-size: 1.1em; color: #333; } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>Real-Time Voice Translator</h1> | |
| <div id="controls"> | |
| <button id="startButton">Start Translation</button> | |
| <button id="stopButton" disabled>Stop Translation</button> | |
| </div> | |
| <p id="status">Status: Not connected</p> | |
| <div id="log"></div> | |
| <script> | |
| const startButton = document.getElementById('startButton'); | |
| const stopButton = document.getElementById('stopButton'); | |
| const status = document.getElementById('status'); | |
| let socket; | |
| let mediaRecorder; | |
| let audioContext; | |
| let audioQueue = []; | |
| let isPlaying = false; | |
| const connectWebSocket = () => { | |
| const proto = window.location.protocol === "https:" ? "wss:" : "ws:"; | |
| const wsUri = `${proto}//${window.location.host}/ws`; | |
| console.log("[CLIENT] Attempting to connect to WebSocket:", wsUri); | |
| status.textContent = `Status: Connecting to ${wsUri}...`; | |
| socket = new WebSocket(wsUri); | |
| socket.onopen = () => { | |
| status.textContent = 'Status: Connected. Ready to start.'; | |
| console.log("[CLIENT] WebSocket connection opened. Enabling start button."); | |
| startButton.disabled = false; | |
| }; | |
| socket.onmessage = (event) => { | |
| if (event.data instanceof Blob) { | |
| const reader = new FileReader(); | |
| reader.onload = function() { | |
| // The server sends raw PCM; we need to wrap it in a WAV header | |
| const pcmData = new Int16Array(this.result); | |
| const wavBlob = createWavBlob(pcmData, 1, 16000); | |
| if (audioContext) { | |
| audioQueue.push(wavBlob); | |
| if (!isPlaying) playNextInQueue(); | |
| } | |
| }; | |
| reader.readAsArrayBuffer(event.data); | |
| } else { | |
| // Handle text messages from server (e.g., for logging) | |
| const logElement = document.createElement('p'); | |
| logElement.textContent = event.data; | |
| document.getElementById('log').prepend(logElement); | |
| } | |
| }; | |
| socket.onclose = () => { | |
| console.log("[CLIENT] WebSocket connection closed."); | |
| status.textContent = 'Status: Disconnected. Please refresh the page.'; | |
| startButton.disabled = false; // Allow user to try starting again | |
| stopButton.disabled = true; | |
| }; | |
| socket.onerror = (error) => { | |
| console.error("[CLIENT] WebSocket Error:", error); | |
| status.textContent = 'Status: Connection error. Check console for details.'; | |
| }; | |
| }; | |
| const playNextInQueue = async () => { | |
| if (audioQueue.length > 0) { | |
| isPlaying = true; | |
| const blob = audioQueue.shift(); | |
| try { | |
| const arrayBuffer = await blob.arrayBuffer(); | |
| const audioBuffer = await audioContext.decodeAudioData(arrayBuffer); | |
| const source = audioContext.createBufferSource(); | |
| source.buffer = audioBuffer; | |
| source.connect(audioContext.destination); | |
| source.onended = playNextInQueue; // Chain the next playback | |
| source.start(); | |
| } catch (e) { | |
| console.error("Error decoding or playing audio:", e); | |
| isPlaying = false; | |
| playNextInQueue(); // Try the next one | |
| } | |
| } else { | |
| isPlaying = false; | |
| } | |
| }; | |
| // Helper function to create a WAV blob from raw PCM data | |
| const createWavBlob = (pcmData, numChannels, sampleRate) => { | |
| const header = new ArrayBuffer(44); | |
| const view = new DataView(header); | |
| const pcmLength = pcmData.length * 2; // 16-bit samples | |
| // RIFF header | |
| view.setUint32(0, 0x52494646, false); // "RIFF" | |
| view.setUint32(4, 36 + pcmLength, true); | |
| view.setUint32(8, 0x57415645, false); // "WAVE" | |
| // "fmt " sub-chunk | |
| view.setUint32(12, 0x666d7420, false); // "fmt " | |
| view.setUint32(16, 16, true); // Sub-chunk size | |
| view.setUint16(20, 1, true); // Audio format (1 for PCM) | |
| view.setUint16(22, numChannels, true); | |
| view.setUint32(24, sampleRate, true); | |
| view.setUint32(28, sampleRate * numChannels * 2, true); // Byte rate | |
| view.setUint16(32, numChannels * 2, true); // Block align | |
| view.setUint16(34, 16, true); // Bits per sample | |
| view.setUint32(36, 0x64617461, false); // "data" | |
| view.setUint32(40, pcmLength, true); | |
| return new Blob([header, pcmData], { type: 'audio/wav' }); | |
| }; | |
| startButton.onclick = async () => { | |
| console.log("[CLIENT] Start button clicked."); | |
| // AudioContext must be created or resumed by a user gesture. | |
| if (!audioContext) { | |
| audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 }); | |
| } else if (audioContext.state === 'suspended') { | |
| await audioContext.resume(); | |
| } | |
| console.log("[CLIENT] Requesting microphone access..."); | |
| navigator.mediaDevices.getUserMedia({ audio: { sampleRate: 16000, channelCount: 1 } }) | |
| .then(stream => { | |
| mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm; codecs=opus' }); | |
| mediaRecorder.ondataavailable = event => { | |
| if (event.data.size > 0 && socket.readyState === WebSocket.OPEN) { | |
| socket.send(event.data); | |
| } | |
| }; | |
| mediaRecorder.start(250); // Send data every 250ms | |
| console.log("[CLIENT] Microphone access granted. MediaRecorder started."); | |
| startButton.disabled = true; | |
| stopButton.disabled = false; | |
| status.textContent = 'Status: Translating...'; | |
| }) | |
| .catch(err => { | |
| console.error('[CLIENT] Error getting user media:', err); | |
| status.textContent = 'Error: Could not access microphone.'; | |
| }); | |
| }; | |
| stopButton.onclick = () => { | |
| console.log("[CLIENT] Stop button clicked."); | |
| if (mediaRecorder) { | |
| mediaRecorder.stop(); | |
| } | |
| // Don't close the socket, just stop sending data. | |
| // The user might want to start and stop multiple times in one session. | |
| startButton.disabled = false; | |
| stopButton.disabled = true; | |
| status.textContent = 'Status: Stopped. Press Start to translate again.'; | |
| }; | |
| window.onload = () => { | |
| console.log("[CLIENT] Page loaded. Initializing..."); | |
| startButton.disabled = true; | |
| stopButton.disabled = true; | |
| connectWebSocket(); // Connect automatically on page load | |
| }; | |
| </script> | |
| </body> | |
| </html> | |