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> | |