RealtimeTranslator / index.html
Mike W
Fix: Initial runtime errors with integration
2e0855f
<!DOCTYPE html>
<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>