|
|
<!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; } |
|
|
#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> |
|
|
|
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
|
<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> |
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
const socket = io(); |
|
|
|
|
|
|
|
|
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(); |
|
|
} |
|
|
|
|
|
|
|
|
async function startAudioProcessing() { |
|
|
|
|
|
|
|
|
audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 24000 }); |
|
|
|
|
|
|
|
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); |
|
|
|
|
|
|
|
|
|
|
|
mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' }); |
|
|
|
|
|
mediaRecorder.ondataavailable = (event) => { |
|
|
if (event.data.size > 0) { |
|
|
socket.emit('client_audio', event.data); |
|
|
} |
|
|
}; |
|
|
|
|
|
mediaRecorder.start(500); |
|
|
statusDiv.textContent = 'Connection active. Speak now!'; |
|
|
} |
|
|
|
|
|
function playNextInQueue() { |
|
|
if (isPlaying || audioQueue.length === 0) { |
|
|
return; |
|
|
} |
|
|
isPlaying = true; |
|
|
const audioData = audioQueue.shift(); |
|
|
|
|
|
|
|
|
audioContext.decodeAudioData(audioData, (buffer) => { |
|
|
const source = audioContext.createBufferSource(); |
|
|
source.buffer = buffer; |
|
|
source.connect(audioContext.destination); |
|
|
source.onended = () => { |
|
|
isPlaying = false; |
|
|
playNextInQueue(); |
|
|
}; |
|
|
source.start(); |
|
|
}, (error) => { |
|
|
console.error('Error decoding audio data:', error); |
|
|
isPlaying = false; |
|
|
playNextInQueue(); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
startButton.addEventListener('click', async () => { |
|
|
statusDiv.textContent = "Connecting to Alexa..."; |
|
|
try { |
|
|
|
|
|
initThree(); |
|
|
await startAudioProcessing(); |
|
|
overlay.style.display = 'none'; |
|
|
chatContainer.style.visibility = 'visible'; |
|
|
} 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 = ''; |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
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) => { |
|
|
|
|
|
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> |