minimal: только Whisper, один файл server.js
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Say — Whisper test</title>
|
||||
<link rel="icon" href="/favicon.png" type="image/png">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<div class="topbar-inner">
|
||||
<a href="/" class="topbar-logo">
|
||||
<img src="/nubes-logo.svg" alt="Nubes" width="130" height="28">
|
||||
</a>
|
||||
<span class="topbar-sep">|</span>
|
||||
<span class="topbar-title">Say — Whisper test</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="page">
|
||||
<div class="card">
|
||||
<div class="card-header">🎤 Проверка Whisper</div>
|
||||
<div class="card-body">
|
||||
<button id="recordBtn" class="btn btn-primary">
|
||||
<span>🎤</span> Записать
|
||||
</button>
|
||||
<span id="status"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="resultCard" style="display:none">
|
||||
<div class="card-header">📝 Результат</div>
|
||||
<div class="card-body" id="result"></div>
|
||||
</div>
|
||||
|
||||
<div id="error" class="alert alert-error" style="display:none"></div>
|
||||
</div>
|
||||
|
||||
<div class="footer">Say v1.0.0 · Whisper</div>
|
||||
|
||||
<script>
|
||||
const btn = document.getElementById('recordBtn');
|
||||
const statusEl = document.getElementById('status');
|
||||
const resultCard = document.getElementById('resultCard');
|
||||
const resultEl = document.getElementById('result');
|
||||
const errorEl = document.getElementById('error');
|
||||
|
||||
let recorder, stream, chunks = [], recording = false;
|
||||
|
||||
async function init() {
|
||||
try {
|
||||
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
btn.disabled = false;
|
||||
} catch (e) {
|
||||
btn.textContent = 'Нет микрофона';
|
||||
}
|
||||
}
|
||||
|
||||
btn.onclick = () => {
|
||||
if (!recording) start();
|
||||
else stop();
|
||||
};
|
||||
|
||||
function start() {
|
||||
chunks = [];
|
||||
recorder = new MediaRecorder(stream, { mimeType: 'audio/webm' });
|
||||
recorder.ondataavailable = e => { if (e.data.size) chunks.push(e.data); };
|
||||
recorder.onstop = send;
|
||||
recorder.start();
|
||||
recording = true;
|
||||
btn.innerHTML = '<span>⏺️</span> Стоп';
|
||||
btn.classList.add('recording');
|
||||
statusEl.textContent = '';
|
||||
resultCard.style.display = 'none';
|
||||
errorEl.style.display = 'none';
|
||||
}
|
||||
|
||||
function stop() {
|
||||
recorder.stop();
|
||||
stream.getTracks().forEach(t => t.stop());
|
||||
recording = false;
|
||||
btn.innerHTML = '<span>⏳</span> Обработка...';
|
||||
btn.disabled = true;
|
||||
}
|
||||
|
||||
async function send() {
|
||||
statusEl.textContent = 'Отправка...';
|
||||
const blob = new Blob(chunks, { type: 'audio/webm' });
|
||||
const fd = new FormData();
|
||||
fd.append('audio', blob, 'rec.webm');
|
||||
|
||||
try {
|
||||
const r = await fetch('/api/speech', { method: 'POST', body: fd });
|
||||
const d = await r.json();
|
||||
if (!r.ok) throw new Error(d.error);
|
||||
resultEl.textContent = d.transcription || '(тишина)';
|
||||
resultCard.style.display = '';
|
||||
statusEl.textContent = 'Готово';
|
||||
} catch (e) {
|
||||
errorEl.textContent = '❌ ' + e.message;
|
||||
errorEl.style.display = '';
|
||||
} finally {
|
||||
btn.innerHTML = '<span>🎤</span> Записать';
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('recording');
|
||||
init();
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user