minimal: только Whisper, один файл server.js

This commit is contained in:
2026-05-31 19:18:34 +03:00
parent a7bf18939c
commit ce88c0c224
11 changed files with 195 additions and 640 deletions
+114
View File
@@ -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>