115 lines
3.3 KiB
HTML
115 lines
3.3 KiB
HTML
<!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>
|