Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Mini-Omni Chat Demo</title> | |
| <style> | |
| body { | |
| background-color: black; | |
| color: white; | |
| font-family: Arial, sans-serif; | |
| } | |
| #chat-container { | |
| height: 300px; | |
| overflow-y: auto; | |
| border: 1px solid #444; | |
| padding: 10px; | |
| margin-bottom: 10px; | |
| } | |
| #status-message { | |
| margin-bottom: 10px; | |
| } | |
| button { | |
| margin-right: 10px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="svg-container"></div> | |
| <div id="chat-container"></div> | |
| <div id="status-message">Current status: idle</div> | |
| <button id="start-button">Start</button> | |
| <button id="stop-button" disabled>Stop</button> | |
| <main> | |
| <p id="current-status">Current status: idle</p> | |
| </main> | |
| </body> | |
| <script> | |
| // Load the SVG | |
| const svgContainer = document.getElementById('svg-container'); | |
| const svgContent = ` | |
| <svg width="800" height="600" viewBox="0 0 800 600" xmlns="http://www.w3.org/2000/svg"> | |
| <ellipse id="left-eye" cx="340" cy="200" rx="20" ry="20" fill="white"/> | |
| <circle id="left-pupil" cx="340" cy="200" r="8" fill="black"/> | |
| <ellipse id="right-eye" cx="460" cy="200" rx="20" ry="20" fill="white"/> | |
| <circle id="right-pupil" cx="460" cy="200" r="8" fill="black"/> | |
| <path id="upper-lip" d="M 300 300 C 350 284, 450 284, 500 300" stroke="white" stroke-width="10" fill="none"/> | |
| <path id="lower-lip" d="M 300 300 C 350 316, 450 316, 500 300" stroke="white" stroke-width="10" fill="none"/> | |
| </svg>`; | |
| svgContainer.innerHTML = svgContent; | |
| // Set up audio context | |
| const audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| const analyser = audioContext.createAnalyser(); | |
| analyser.fftSize = 256; | |
| // Animation variables | |
| let isAudioPlaying = false; | |
| let lastBlinkTime = 0; | |
| let eyeMovementOffset = { x: 0, y: 0 }; | |
| // Chat variables | |
| let mediaRecorder; | |
| let audioChunks = []; | |
| let isRecording = false; | |
| const API_URL = 'http://127.0.0.1:60808/chat'; | |
| // Idle eye animation function | |
| function animateIdleEyes(timestamp) { | |
| const leftEye = document.getElementById('left-eye'); | |
| const rightEye = document.getElementById('right-eye'); | |
| const leftPupil = document.getElementById('left-pupil'); | |
| const rightPupil = document.getElementById('right-pupil'); | |
| const baseEyeX = { left: 340, right: 460 }; | |
| const baseEyeY = 200; | |
| // Blink effect | |
| const blinkInterval = 4000 + Math.random() * 2000; // Random blink interval between 4-6 seconds | |
| if (timestamp - lastBlinkTime > blinkInterval) { | |
| leftEye.setAttribute('ry', '2'); | |
| rightEye.setAttribute('ry', '2'); | |
| leftPupil.setAttribute('ry', '0.8'); | |
| rightPupil.setAttribute('ry', '0.8'); | |
| setTimeout(() => { | |
| leftEye.setAttribute('ry', '20'); | |
| rightEye.setAttribute('ry', '20'); | |
| leftPupil.setAttribute('ry', '8'); | |
| rightPupil.setAttribute('ry', '8'); | |
| }, 150); | |
| lastBlinkTime = timestamp; | |
| } | |
| // Subtle eye movement | |
| const movementSpeed = 0.001; | |
| eyeMovementOffset.x = Math.sin(timestamp * movementSpeed) * 6; | |
| eyeMovementOffset.y = Math.cos(timestamp * movementSpeed * 1.3) * 1; // Reduced vertical movement | |
| leftEye.setAttribute('cx', baseEyeX.left + eyeMovementOffset.x); | |
| leftEye.setAttribute('cy', baseEyeY + eyeMovementOffset.y); | |
| rightEye.setAttribute('cx', baseEyeX.right + eyeMovementOffset.x); | |
| rightEye.setAttribute('cy', baseEyeY + eyeMovementOffset.y); | |
| leftPupil.setAttribute('cx', baseEyeX.left + eyeMovementOffset.x); | |
| leftPupil.setAttribute('cy', baseEyeY + eyeMovementOffset.y); | |
| rightPupil.setAttribute('cx', baseEyeX.right + eyeMovementOffset.x); | |
| rightPupil.setAttribute('cy', baseEyeY + eyeMovementOffset.y); | |
| } | |
| // Main animation function | |
| function animate(timestamp) { | |
| if (isAudioPlaying) { | |
| const dataArray = new Uint8Array(analyser.frequencyBinCount); | |
| analyser.getByteFrequencyData(dataArray); | |
| // Calculate the average amplitude in the speech frequency range | |
| const speechRange = dataArray.slice(5, 80); // Adjust based on your needs | |
| const averageAmplitude = speechRange.reduce((a, b) => a + b) / speechRange.length; | |
| // Normalize the amplitude (0-1 range) | |
| const normalizedAmplitude = averageAmplitude / 255; | |
| // Animate mouth | |
| const upperLip = document.getElementById('upper-lip'); | |
| const lowerLip = document.getElementById('lower-lip'); | |
| const baseY = 300; | |
| const maxMovement = 60; | |
| const newUpperY = baseY - normalizedAmplitude * maxMovement; | |
| const newLowerY = baseY + normalizedAmplitude * maxMovement; | |
| // Adjust control points for more natural movement | |
| const upperControlY1 = newUpperY - 8; | |
| const upperControlY2 = newUpperY - 8; | |
| const lowerControlY1 = newLowerY + 8; | |
| const lowerControlY2 = newLowerY + 8; | |
| upperLip.setAttribute('d', `M 300 ${baseY} C 350 ${upperControlY1}, 450 ${upperControlY2}, 500 ${baseY}`); | |
| lowerLip.setAttribute('d', `M 300 ${baseY} C 350 ${lowerControlY1}, 450 ${lowerControlY2}, 500 ${baseY}`); | |
| // Animate eyes | |
| const leftEye = document.getElementById('left-eye'); | |
| const rightEye = document.getElementById('right-eye'); | |
| const leftPupil = document.getElementById('left-pupil'); | |
| const rightPupil = document.getElementById('right-pupil'); | |
| const baseEyeY = 200; | |
| const maxEyeMovement = 10; | |
| const newEyeY = baseEyeY - normalizedAmplitude * maxEyeMovement; | |
| leftEye.setAttribute('cy', newEyeY); | |
| rightEye.setAttribute('cy', newEyeY); | |
| leftPupil.setAttribute('cy', newEyeY); | |
| rightPupil.setAttribute('cy', newEyeY); | |
| } else { | |
| animateIdleEyes(timestamp); | |
| } | |
| requestAnimationFrame(animate); | |
| } | |
| // Start animation | |
| animate(); | |
| // Chat functions | |
| function startRecording() { | |
| navigator.mediaDevices.getUserMedia({ audio: true }) | |
| .then(stream => { | |
| mediaRecorder = new MediaRecorder(stream); | |
| mediaRecorder.ondataavailable = event => { | |
| audioChunks.push(event.data); | |
| }; | |
| mediaRecorder.onstop = sendAudioToServer; | |
| mediaRecorder.start(); | |
| isRecording = true; | |
| updateStatus('Recording...'); | |
| document.getElementById('start-button').disabled = true; | |
| document.getElementById('stop-button').disabled = false; | |
| }) | |
| .catch(error => { | |
| console.error('Error accessing microphone:', error); | |
| updateStatus('Error: ' + error.message); | |
| }); | |
| } | |
| function stopRecording() { | |
| if (mediaRecorder && isRecording) { | |
| mediaRecorder.stop(); | |
| isRecording = false; | |
| updateStatus('Processing...'); | |
| document.getElementById('start-button').disabled = false; | |
| document.getElementById('stop-button').disabled = true; | |
| } | |
| } | |
| function sendAudioToServer() { | |
| const audioBlob = new Blob(audioChunks, { type: 'audio/wav' }); | |
| const reader = new FileReader(); | |
| reader.readAsDataURL(audioBlob); | |
| reader.onloadend = function() { | |
| const base64Audio = reader.result.split(',')[1]; | |
| fetch(API_URL, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ audio: base64Audio }), | |
| }) | |
| .then(response => response.blob()) | |
| .then(blob => { | |
| const audioUrl = URL.createObjectURL(blob); | |
| playResponseAudio(audioUrl); | |
| updateChatHistory('User', 'Audio message sent'); | |
| updateChatHistory('Assistant', 'Audio response received'); | |
| }) | |
| .catch(error => { | |
| console.error('Error:', error); | |
| updateStatus('Error: ' + error.message); | |
| }); | |
| }; | |
| audioChunks = []; | |
| } | |
| function playResponseAudio(audioUrl) { | |
| const audio = new Audio(audioUrl); | |
| audio.onloadedmetadata = () => { | |
| const source = audioContext.createMediaElementSource(audio); | |
| source.connect(analyser); | |
| analyser.connect(audioContext.destination); | |
| }; | |
| audio.onplay = () => { | |
| isAudioPlaying = true; | |
| updateStatus('Playing response...'); | |
| }; | |
| audio.onended = () => { | |
| isAudioPlaying = false; | |
| updateStatus('Idle'); | |
| }; | |
| audio.play(); | |
| } | |
| function updateChatHistory(role, message) { | |
| const chatContainer = document.getElementById('chat-container'); | |
| const messageElement = document.createElement('p'); | |
| messageElement.textContent = `${role}: ${message}`; | |
| chatContainer.appendChild(messageElement); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| } | |
| function updateStatus(status) { | |
| document.getElementById('status-message').textContent = status; | |
| document.getElementById('current-status').textContent = 'Current status: ' + status; | |
| } | |
| // Event listeners | |
| document.getElementById('start-button').addEventListener('click', startRecording); | |
| document.getElementById('stop-button').addEventListener('click', stopRecording); | |
| // Initialize | |
| updateStatus('Idle'); | |
| </script> | |
| </html> | |