File size: 10,534 Bytes
ab305ca | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 | <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Alexa - Live Connect</title>
<style>
body { margin: 0; overflow: hidden; background-color: #0a0a0a; color: #00ff00; font-family: 'Courier New', Courier, monospace; }
canvas { display: block; }
#overlay-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 20; }
#start-button { font-size: 1.5em; padding: 15px 30px; background-color: #00ff00; color: #0a0a0a; border: none; cursor: pointer; font-family: 'Courier New', Courier, monospace; box-shadow: 0 0 20px rgba(0, 255, 0, 0.7); }
#start-button:hover { background-color: #55ff55; }
#status { margin-top: 20px; font-size: 1.2em; text-shadow: 0 0 5px #00ff00; }
#chat-container { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); width: 80%; max-width: 600px; z-index: 10; display: flex; flex-direction: column; align-items: center; visibility: hidden; /* Hidden by default */ }
#chat-input { width: 100%; padding: 10px; border: none; border-radius: 5px; background-color: rgba(50, 50, 70, 0.8); color: #00ff00; font-family: 'Courier New', Courier, monospace; font-size: 1em; outline: none; box-shadow: 0 0 10px rgba(0, 255, 0, 0.5); }
#chat-input::placeholder { color: rgba(0, 255, 0, 0.5); }
</style>
</head>
<body>
<canvas id="three-canvas"></canvas>
<!-- Overlay for starting the session -->
<div id="overlay-container">
<button id="start-button">Click to Awaken Alexa</button>
<div id="status">Waiting to connect...</div>
</div>
<div id="chat-container">
<input type="text" id="chat-input" placeholder="Say something...">
</div>
<!-- Dependencies -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/0.158.0/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.5/socket.io.min.js"></script>
<script>
// --- Global Variables ---
const canvas = document.getElementById('three-canvas');
const chatInput = document.getElementById('chat-input');
const startButton = document.getElementById('start-button');
const statusDiv = document.getElementById('status');
const overlay = document.getElementById('overlay-container');
const chatContainer = document.getElementById('chat-container');
let chatMessages = [];
const maxMessages = 10;
let scene, camera, renderer, terminalMesh, terminalTexture, textContext;
let mediaRecorder;
let audioContext;
let audioQueue = [];
let isPlaying = false;
// --- Socket.IO Connection ---
const socket = io();
// --- Core 3D Setup (from your original code, slightly adapted) ---
function initThree() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a0a);
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 1.5, 3.5);
renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
const ambientLight = new THREE.AmbientLight(0x404040, 2);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
scene.add(directionalLight);
const terminalGeometry = new THREE.BoxGeometry(4, 2.5, 0.1);
const textCanvas = document.createElement('canvas');
textCanvas.width = 512;
textCanvas.height = 512;
textContext = textCanvas.getContext('2d');
terminalTexture = new THREE.CanvasTexture(textCanvas);
const terminalMaterial = new THREE.MeshStandardMaterial({
map: terminalTexture,
color: 0x222222,
emissive: 0x00ff00,
emissiveIntensity: 0.8,
metalness: 0.1,
roughness: 0.8,
});
terminalMesh = new THREE.Mesh(terminalGeometry, terminalMaterial);
terminalMesh.position.set(0, 1.5, 0);
scene.add(terminalMesh);
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}, false);
animate();
addMessageToTerminal("System: Initializing...");
}
function animate() {
requestAnimationFrame(animate);
terminalMesh.rotation.y += 0.0005;
renderer.render(scene, camera);
}
function updateTerminalTexture() {
textContext.fillStyle = '#0a0a0a';
textContext.fillRect(0, 0, textContext.canvas.width, textContext.canvas.height);
textContext.font = 'Bold 30px Courier New';
textContext.fillStyle = '#00ff00';
textContext.textAlign = 'left';
textContext.textBaseline = 'top';
const padding = 20;
const lineHeight = 35;
let y = padding;
chatMessages.forEach(msg => {
textContext.fillText(msg, padding, y);
y += lineHeight;
});
terminalTexture.needsUpdate = true;
}
function addMessageToTerminal(message) {
chatMessages.push(message);
if (chatMessages.length > maxMessages) {
chatMessages.shift();
}
updateTerminalTexture();
}
// --- Web Audio and Media Recorder Setup ---
async function startAudioProcessing() {
// 1. Create AudioContext for playback (required by browsers to start after user interaction)
// The Gemini model sends audio at 24000 Hz.
audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 24000 });
// 2. Get microphone access
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
// 3. Setup MediaRecorder to capture audio and send it to the server
// The `timeslice` parameter makes it emit data every 500ms.
mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' });
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
socket.emit('client_audio', event.data);
}
};
mediaRecorder.start(500); // Send data every 500ms
statusDiv.textContent = 'Connection active. Speak now!';
}
function playNextInQueue() {
if (isPlaying || audioQueue.length === 0) {
return;
}
isPlaying = true;
const audioData = audioQueue.shift();
// Decode the ArrayBuffer into an AudioBuffer
audioContext.decodeAudioData(audioData, (buffer) => {
const source = audioContext.createBufferSource();
source.buffer = buffer;
source.connect(audioContext.destination);
source.onended = () => {
isPlaying = false;
playNextInQueue(); // Play the next chunk when this one is done
};
source.start();
}, (error) => {
console.error('Error decoding audio data:', error);
isPlaying = false;
playNextInQueue(); // Skip bad chunk
});
}
// --- Event Listeners ---
startButton.addEventListener('click', async () => {
statusDiv.textContent = "Connecting to Alexa...";
try {
// Initialize 3D scene and audio after user clicks
initThree();
await startAudioProcessing();
overlay.style.display = 'none'; // Hide the start button
chatContainer.style.visibility = 'visible'; // Show the chat input
} catch (error) {
console.error("Error starting session:", error);
statusDiv.textContent = "Error: Could not access microphone.";
addMessageToTerminal("Error: Mic access denied.");
}
});
chatInput.addEventListener('keypress', (event) => {
if (event.key === 'Enter') {
const message = chatInput.value.trim();
if (message) {
addMessageToTerminal("You: " + message);
socket.emit('client_text', { text: message });
chatInput.value = '';
}
}
});
// --- SocketIO Event Listeners ---
socket.on('connect', () => {
console.log('Socket.IO connected!');
statusDiv.textContent = 'Connected. Waiting for session...';
});
socket.on('session_ready', () => {
console.log('Gemini session is ready on the server.');
addMessageToTerminal('Alexa: Hey there! What\'s on your mind? ;)');
});
socket.on('server_text', (data) => {
console.log('Received text:', data.text);
addMessageToTerminal("Alexa: " + data.text);
});
socket.on('server_audio', (data) => {
// data is an ArrayBuffer. Add it to our queue for smooth playback.
audioQueue.push(data);
playNextInQueue();
});
socket.on('disconnect', () => {
console.log('Socket.IO disconnected.');
statusDiv.textContent = 'Disconnected.';
addMessageToTerminal("System: Connection lost.");
if (mediaRecorder && mediaRecorder.state === 'recording') {
mediaRecorder.stop();
}
});
socket.on('error', (data) => {
console.error('Server error:', data.message);
statusDiv.textContent = `Error: ${data.message}`;
addMessageToTerminal(`System Error: ${data.message}`);
});
</script>
</body>
</html> |