|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<title>Voice Agent</title> |
|
<style> |
|
.call-button { |
|
padding: 20px 40px; |
|
font-size: 24px; |
|
border-radius: 50px; |
|
background-color: #4CAF50; |
|
color: white; |
|
border: none; |
|
cursor: pointer; |
|
transition: all 0.3s; |
|
} |
|
|
|
.call-button.active { |
|
background-color: #f44336; |
|
} |
|
|
|
.status { |
|
margin-top: 20px; |
|
font-size: 18px; |
|
} |
|
|
|
.volume-meter { |
|
width: 300px; |
|
height: 20px; |
|
background-color: #ddd; |
|
margin: 20px auto; |
|
border-radius: 10px; |
|
overflow: hidden; |
|
} |
|
|
|
.volume-level { |
|
height: 100%; |
|
width: 0%; |
|
background-color: #4CAF50; |
|
transition: width 0.1s; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div style="text-align: center; padding: 50px;"> |
|
<button id="callButton" class="call-button">Start Call</button> |
|
<div class="volume-meter"> |
|
<div id="volumeLevel" class="volume-level"></div> |
|
</div> |
|
<div id="status" class="status"></div> |
|
</div> |
|
|
|
<script> |
|
let ws, mediaRecorder, audioChunks = []; |
|
let isListening = false; |
|
let isPlayingResponse = false; |
|
let audioContext, analyser, dataArray; |
|
const silenceThreshold = -40; |
|
const silenceTime = 2.0; |
|
let lastAudioLevel = Date.now(); |
|
|
|
const callButton = document.getElementById('callButton'); |
|
const statusDiv = document.getElementById('status'); |
|
const volumeLevel = document.getElementById('volumeLevel'); |
|
|
|
async function startCall() { |
|
try { |
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); |
|
setupAudioAnalyser(stream); |
|
ws = new WebSocket(`wss://puzan789-arkos.hf.space/ws/voicechat`); |
|
|
|
ws.onopen = () => { |
|
statusDiv.textContent = 'Connected - Ready to listen'; |
|
callButton.classList.add('active'); |
|
callButton.textContent = 'End Call'; |
|
startListening(stream); |
|
}; |
|
|
|
ws.onmessage = async (event) => { |
|
const data = JSON.parse(event.data); |
|
stopListening(); |
|
|
|
if (data.audio) { |
|
statusDiv.innerHTML = `<b>You said:</b> ${data.transcript}<br><b>AI is responding...</b>`; |
|
|
|
|
|
const binaryString = atob(data.audio); |
|
const bytes = new Uint8Array(binaryString.length); |
|
for (let i = 0; i < binaryString.length; i++) { |
|
bytes[i] = binaryString.charCodeAt(i); |
|
} |
|
|
|
const audioBlob = new Blob([bytes], { type: 'audio/wav' }); |
|
const audioUrl = URL.createObjectURL(audioBlob); |
|
const audio = new Audio(audioUrl); |
|
|
|
audio.onended = () => { |
|
ws.send("audio_complete"); |
|
statusDiv.innerHTML = `<b>You said:</b> ${data.transcript}<br><b>AI responded:</b> ${data.response}<br><b>Ready for next input</b>`; |
|
startListening(stream); |
|
}; |
|
|
|
audio.play(); |
|
} |
|
}; |
|
|
|
ws.onclose = () => stopCall(); |
|
|
|
} catch (error) { |
|
console.error('Error:', error); |
|
statusDiv.textContent = 'Error: ' + error.message; |
|
} |
|
} |
|
|
|
function setupAudioAnalyser(stream) { |
|
audioContext = new AudioContext(); |
|
analyser = audioContext.createAnalyser(); |
|
const source = audioContext.createMediaStreamSource(stream); |
|
source.connect(analyser); |
|
analyser.fftSize = 2048; |
|
dataArray = new Float32Array(analyser.frequencyBinCount); |
|
} |
|
|
|
function getAudioLevel() { |
|
analyser.getFloatTimeDomainData(dataArray); |
|
let sum = 0; |
|
for (let i = 0; i < dataArray.length; i++) { |
|
sum += dataArray[i] * dataArray[i]; |
|
} |
|
const rms = Math.sqrt(sum / dataArray.length); |
|
const db = 20 * Math.log10(rms); |
|
return db; |
|
} |
|
|
|
function updateVolumeMeter() { |
|
if (analyser && isListening) { |
|
const db = getAudioLevel(); |
|
const normalizedDb = Math.max(0, Math.min(100, (db + 60) * 2)); |
|
volumeLevel.style.width = normalizedDb + '%'; |
|
|
|
if (db > silenceThreshold) { |
|
lastAudioLevel = Date.now(); |
|
} else if (Date.now() - lastAudioLevel > silenceTime * 1000) { |
|
sendAudio(); |
|
lastAudioLevel = Date.now(); |
|
} |
|
} |
|
requestAnimationFrame(updateVolumeMeter); |
|
} |
|
|
|
function startListening(stream) { |
|
isListening = true; |
|
mediaRecorder = new MediaRecorder(stream); |
|
mediaRecorder.ondataavailable = async (event) => { |
|
if (event.data.size > 0) { |
|
audioChunks.push(event.data); |
|
} |
|
}; |
|
mediaRecorder.start(250); |
|
updateVolumeMeter(); |
|
} |
|
|
|
function stopListening() { |
|
isListening = false; |
|
if (mediaRecorder && mediaRecorder.state === 'recording') { |
|
mediaRecorder.stop(); |
|
} |
|
audioChunks = []; |
|
volumeLevel.style.width = '0%'; |
|
} |
|
|
|
async function sendAudio() { |
|
if (audioChunks.length > 0 && ws.readyState === WebSocket.OPEN) { |
|
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' }); |
|
ws.send(await audioBlob.arrayBuffer()); |
|
audioChunks = []; |
|
stopListening(); |
|
} |
|
} |
|
|
|
function stopCall() { |
|
if (ws) ws.close(); |
|
stopListening(); |
|
if (audioContext) audioContext.close(); |
|
callButton.classList.remove('active'); |
|
callButton.textContent = 'Start Call'; |
|
} |
|
|
|
callButton.addEventListener('click', () => { |
|
if (!ws || ws.readyState === WebSocket.CLOSED) { |
|
startCall(); |
|
} else { |
|
stopCall(); |
|
} |
|
}); |
|
</script> |
|
</body> |
|
</html> |