omni-docker / webui /index.html
victor's picture
victor HF Staff
feat: implement pure web demo with audio recording and chat functionality
e1c3a1a
raw
history blame
8.82 kB
<!DOCTYPE html>
<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>