307 lines
9.6 KiB
JavaScript
307 lines
9.6 KiB
JavaScript
const character = new Character('character-canvas');
|
|
const audioHandler = new AudioHandler();
|
|
|
|
const recordBtn = document.getElementById('record-btn');
|
|
const btnText = document.getElementById('btn-text');
|
|
const wakeBtn = document.getElementById('wake-btn');
|
|
const wakeText = document.getElementById('wake-text');
|
|
const statusEl = document.getElementById('status');
|
|
const userTextEl = document.getElementById('user-text');
|
|
const botTextEl = document.getElementById('bot-text');
|
|
|
|
let ws = null;
|
|
let wakeListening = false;
|
|
let wakeRecorder = null;
|
|
let wakeStream = null;
|
|
let processingAudio = false;
|
|
let waitingForWakeResponse = false;
|
|
|
|
function connectWebSocket() {
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
ws = new WebSocket(`${protocol}//${window.location.host}/ws`);
|
|
|
|
ws.onopen = () => {
|
|
statusEl.textContent = 'Conectat! Spune "EcoBot" sau apasa butonul.';
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
statusEl.textContent = 'Deconectat. Se reconecteaza...';
|
|
setTimeout(connectWebSocket, 2000);
|
|
};
|
|
|
|
ws.onmessage = async (event) => {
|
|
const message = JSON.parse(event.data);
|
|
|
|
switch (message.type) {
|
|
case 'status':
|
|
statusEl.textContent = message.text;
|
|
if (message.text.includes('Ascult')) {
|
|
character.setState('listening');
|
|
} else if (message.text.includes('gandesc')) {
|
|
character.setState('thinking');
|
|
}
|
|
break;
|
|
|
|
case 'user_text':
|
|
userTextEl.textContent = `Tu: ${message.text}`;
|
|
userTextEl.classList.remove('hidden');
|
|
break;
|
|
|
|
case 'bot_text':
|
|
botTextEl.textContent = `EcoBot: ${message.text}`;
|
|
botTextEl.classList.remove('hidden');
|
|
break;
|
|
|
|
case 'audio_response':
|
|
character.setState('talking');
|
|
statusEl.textContent = 'Vorbesc...';
|
|
await audioHandler.playAudioFromBase64(message.data, message.mime || 'audio/mpeg');
|
|
character.setState('idle');
|
|
processingAudio = false;
|
|
resumeWakeIfActive();
|
|
break;
|
|
|
|
case 'text_only_response':
|
|
botTextEl.textContent = `EcoBot: ${message.text}`;
|
|
botTextEl.classList.remove('hidden');
|
|
character.setState('talking');
|
|
await new Promise(r => setTimeout(r, 2000));
|
|
character.setState('idle');
|
|
processingAudio = false;
|
|
resumeWakeIfActive();
|
|
break;
|
|
|
|
case 'wake_detected':
|
|
waitingForWakeResponse = false;
|
|
statusEl.textContent = 'Te-am auzit! Vorbeste acum...';
|
|
character.setState('listening');
|
|
await recordAndSendQuestion();
|
|
break;
|
|
|
|
case 'wake_not_detected':
|
|
waitingForWakeResponse = false;
|
|
// Only start next chunk after this one is done
|
|
if (wakeListening && !processingAudio) {
|
|
startWakeLoop();
|
|
}
|
|
break;
|
|
}
|
|
};
|
|
}
|
|
|
|
function resumeWakeIfActive() {
|
|
if (wakeListening) {
|
|
statusEl.textContent = 'Ascult... Spune "EcoBot" pentru a activa.';
|
|
startWakeLoop();
|
|
} else {
|
|
statusEl.textContent = 'Spune "EcoBot" sau apasa butonul.';
|
|
}
|
|
}
|
|
|
|
// ---- Wake Word Mode ----
|
|
|
|
function toggleWakeMode() {
|
|
if (wakeListening) {
|
|
stopWakeMode();
|
|
} else {
|
|
startWakeMode();
|
|
}
|
|
}
|
|
|
|
function startWakeMode() {
|
|
wakeListening = true;
|
|
waitingForWakeResponse = false;
|
|
wakeBtn.classList.add('listening');
|
|
wakeText.textContent = 'Ascult... (click pentru a opri)';
|
|
statusEl.textContent = 'Ascult... Spune "EcoBot" pentru a activa.';
|
|
character.setState('idle');
|
|
startWakeLoop();
|
|
}
|
|
|
|
function stopWakeMode() {
|
|
wakeListening = false;
|
|
waitingForWakeResponse = false;
|
|
wakeBtn.classList.remove('listening');
|
|
wakeText.textContent = 'Spune "EcoBot" pentru a activa';
|
|
statusEl.textContent = 'Spune "EcoBot" sau apasa butonul.';
|
|
stopWakeRecorder();
|
|
}
|
|
|
|
async function startWakeLoop() {
|
|
// Don't start a new recording if we're still waiting for the previous response
|
|
if (!wakeListening || processingAudio || waitingForWakeResponse) return;
|
|
|
|
const micId = audioHandler.selectedMicId;
|
|
const constraints = {
|
|
audio: micId
|
|
? { deviceId: { exact: micId }, autoGainControl: true, noiseSuppression: true }
|
|
: { autoGainControl: true, noiseSuppression: true },
|
|
};
|
|
|
|
try {
|
|
wakeStream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
wakeRecorder = new MediaRecorder(wakeStream, { mimeType: 'audio/webm' });
|
|
const chunks = [];
|
|
|
|
wakeRecorder.ondataavailable = (e) => chunks.push(e.data);
|
|
|
|
wakeRecorder.onstop = async () => {
|
|
if (!wakeListening) return;
|
|
|
|
// Mark as waiting — don't record another chunk until server responds
|
|
waitingForWakeResponse = true;
|
|
statusEl.textContent = 'Procesez...';
|
|
|
|
const blob = new Blob(chunks, { type: 'audio/webm' });
|
|
const wavBlob = await audioHandler.convertToWav(blob);
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = () => {
|
|
const base64 = reader.result.split(',')[1];
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify({ type: 'audio', data: base64, mode: 'wake' }));
|
|
}
|
|
};
|
|
reader.readAsDataURL(wavBlob);
|
|
};
|
|
|
|
wakeRecorder.start();
|
|
statusEl.textContent = 'Ascult... Spune "EcoBot"';
|
|
|
|
// Record 3 second chunks
|
|
setTimeout(() => {
|
|
if (wakeRecorder && wakeRecorder.state === 'recording') {
|
|
wakeRecorder.stop();
|
|
if (wakeStream) {
|
|
wakeStream.getTracks().forEach(t => t.stop());
|
|
}
|
|
}
|
|
}, 3000);
|
|
} catch (e) {
|
|
console.error('Wake mode error:', e);
|
|
statusEl.textContent = 'Eroare microfon: ' + e.message;
|
|
stopWakeMode();
|
|
}
|
|
}
|
|
|
|
function stopWakeRecorder() {
|
|
if (wakeRecorder && wakeRecorder.state === 'recording') {
|
|
wakeRecorder.stop();
|
|
}
|
|
if (wakeStream) {
|
|
wakeStream.getTracks().forEach(t => t.stop());
|
|
wakeStream = null;
|
|
}
|
|
wakeRecorder = null;
|
|
}
|
|
|
|
async function recordAndSendQuestion() {
|
|
processingAudio = true;
|
|
stopWakeRecorder();
|
|
|
|
const micId = audioHandler.selectedMicId;
|
|
const constraints = {
|
|
audio: micId
|
|
? { deviceId: { exact: micId }, autoGainControl: true, noiseSuppression: true }
|
|
: { autoGainControl: true, noiseSuppression: true },
|
|
};
|
|
|
|
try {
|
|
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
const recorder = new MediaRecorder(stream, { mimeType: 'audio/webm' });
|
|
const chunks = [];
|
|
|
|
recorder.ondataavailable = (e) => chunks.push(e.data);
|
|
|
|
recorder.onstop = async () => {
|
|
stream.getTracks().forEach(t => t.stop());
|
|
const blob = new Blob(chunks, { type: 'audio/webm' });
|
|
const wavBlob = await audioHandler.convertToWav(blob);
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = () => {
|
|
const base64 = reader.result.split(',')[1];
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify({ type: 'audio', data: base64 }));
|
|
}
|
|
};
|
|
reader.readAsDataURL(wavBlob);
|
|
};
|
|
|
|
statusEl.textContent = 'Te ascult... (5 secunde)';
|
|
recorder.start();
|
|
|
|
setTimeout(() => {
|
|
if (recorder.state === 'recording') {
|
|
recorder.stop();
|
|
statusEl.textContent = 'Procesez...';
|
|
}
|
|
}, 5000);
|
|
} catch (e) {
|
|
console.error('Record error:', e);
|
|
processingAudio = false;
|
|
if (wakeListening) startWakeLoop();
|
|
}
|
|
}
|
|
|
|
// ---- Manual Record (hold button) ----
|
|
|
|
async function startRecording() {
|
|
stopWakeRecorder();
|
|
await audioHandler.init();
|
|
audioHandler.startRecording();
|
|
recordBtn.classList.add('recording');
|
|
btnText.textContent = 'Inregistrez... elibereaza pentru a trimite';
|
|
character.setState('listening');
|
|
statusEl.textContent = 'Te ascult...';
|
|
}
|
|
|
|
async function stopRecording() {
|
|
const audioBlob = await audioHandler.stopRecording();
|
|
recordBtn.classList.remove('recording');
|
|
btnText.textContent = 'Sau tine apasat si vorbeste';
|
|
processingAudio = true;
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = () => {
|
|
const base64 = reader.result.split(',')[1];
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify({ type: 'audio', data: base64 }));
|
|
}
|
|
};
|
|
reader.readAsDataURL(audioBlob);
|
|
}
|
|
|
|
// ---- Event Listeners ----
|
|
|
|
wakeBtn.addEventListener('click', () => toggleWakeMode());
|
|
|
|
recordBtn.addEventListener('mousedown', (e) => {
|
|
e.preventDefault();
|
|
startRecording();
|
|
});
|
|
|
|
recordBtn.addEventListener('mouseup', (e) => {
|
|
e.preventDefault();
|
|
if (audioHandler.isRecording) stopRecording();
|
|
});
|
|
|
|
recordBtn.addEventListener('mouseleave', (e) => {
|
|
if (audioHandler.isRecording) stopRecording();
|
|
});
|
|
|
|
recordBtn.addEventListener('touchstart', (e) => {
|
|
e.preventDefault();
|
|
startRecording();
|
|
});
|
|
|
|
recordBtn.addEventListener('touchend', (e) => {
|
|
e.preventDefault();
|
|
if (audioHandler.isRecording) stopRecording();
|
|
});
|
|
|
|
// Settings
|
|
const settings = new SettingsManager(audioHandler);
|
|
|
|
// Start
|
|
connectWebSocket();
|