feat: Express + Whisper + 3 LLM + UI с микрофоном
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Say — Voice Assistant</title>
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Say</h1>
|
||||
|
||||
<div id="models">
|
||||
<button class="model-btn" data-model="ddm">DDM-120</button>
|
||||
<button class="model-btn" data-model="qwen">Qwen</button>
|
||||
<button class="model-btn active" data-model="deepseek">DeepSeek</button>
|
||||
</div>
|
||||
|
||||
<div id="chat"></div>
|
||||
|
||||
<div id="recorder">
|
||||
<button id="mic-btn">Start Recording</button>
|
||||
<span id="status"></span>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let mediaRecorder, chunks = [];
|
||||
const micBtn = document.getElementById('mic-btn');
|
||||
const status = document.getElementById('status');
|
||||
const chat = document.getElementById('chat');
|
||||
let activeModel = 'deepseek';
|
||||
|
||||
document.querySelectorAll('.model-btn').forEach(b => {
|
||||
b.addEventListener('click', () => {
|
||||
document.querySelectorAll('.model-btn').forEach(x => x.classList.remove('active'));
|
||||
b.classList.add('active');
|
||||
activeModel = b.dataset.model;
|
||||
});
|
||||
});
|
||||
|
||||
micBtn.addEventListener('click', async () => {
|
||||
if (mediaRecorder && mediaRecorder.state === 'recording') {
|
||||
mediaRecorder.stop();
|
||||
micBtn.textContent = 'Processing...';
|
||||
return;
|
||||
}
|
||||
chunks = [];
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
mediaRecorder = new MediaRecorder(stream);
|
||||
mediaRecorder.ondataavailable = e => chunks.push(e.data);
|
||||
mediaRecorder.onstop = async () => {
|
||||
stream.getTracks().forEach(t => t.stop());
|
||||
const blob = new Blob(chunks, { type: 'audio/webm' });
|
||||
const fd = new FormData(); fd.append('audio', blob);
|
||||
status.textContent = 'Transcribing...';
|
||||
const t = await fetch('/api/whisper', { method: 'POST', body: fd }).then(r => r.json());
|
||||
chat.innerHTML += '<div class="user">You: ' + t.text + '</div>';
|
||||
status.textContent = activeModel + ' thinking...';
|
||||
const r = await fetch('/api/chat', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ model: activeModel, message: t.text }),
|
||||
}).then(r => r.json());
|
||||
chat.innerHTML += '<div class="bot">' + activeModel + ': ' + r.response + '</div>';
|
||||
chat.scrollTop = chat.scrollHeight;
|
||||
micBtn.textContent = 'Start Recording';
|
||||
status.textContent = '';
|
||||
};
|
||||
mediaRecorder.start();
|
||||
micBtn.textContent = 'Stop Recording';
|
||||
status.textContent = 'Recording...';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,11 @@
|
||||
body { font-family: system-ui, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; background: #111; color: #eee; }
|
||||
h1 { text-align: center; color: #6c5ce7; }
|
||||
#models { display: flex; gap: 8px; justify-content: center; margin: 16px 0; }
|
||||
.model-btn { padding: 8px 16px; border: 1px solid #6c5ce7; background: transparent; color: #6c5ce7; border-radius: 6px; cursor: pointer; }
|
||||
.model-btn.active { background: #6c5ce7; color: #fff; }
|
||||
#chat { border: 1px solid #333; border-radius: 8px; padding: 16px; min-height: 300px; max-height: 400px; overflow-y: auto; margin: 16px 0; }
|
||||
.user { color: #00cec9; margin: 8px 0; }
|
||||
.bot { color: #a29bfe; margin: 8px 0; }
|
||||
#recorder { text-align: center; }
|
||||
#mic-btn { padding: 12px 32px; background: #e84393; color: #fff; border: none; border-radius: 8px; font-size: 16px; cursor: pointer; }
|
||||
#status { display: inline-block; margin-left: 12px; color: #999; }
|
||||
Reference in New Issue
Block a user