1
0
Fork 0
security-lab/test.html
2026-04-04 21:23:18 +03:00

758 lines
26 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chatterbox TTS Test</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 40px 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
background: white;
border-radius: 20px;
padding: 40px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
font-size: 28px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
color: #555;
font-weight: 500;
font-size: 14px;
}
input[type="text"],
input[type="number"],
select,
textarea {
width: 100%;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 15px;
transition: border-color 0.3s;
}
input:focus,
select:focus,
textarea:focus {
outline: none;
border-color: #667eea;
}
textarea {
resize: vertical;
min-height: 80px;
}
/* Voice Recording Section */
.voice-section {
background: #f8f9ff;
border-radius: 15px;
padding: 25px;
margin-bottom: 25px;
border: 2px solid #e0e0e0;
}
.voice-section.recording {
border-color: #e74c3c;
background: #fdf2f2;
}
.voice-section.has-recording {
border-color: #4caf50;
background: #f1f8f4;
}
.voice-label {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
}
.record-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 50%;
width: 70px;
height: 70px;
font-size: 28px;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
.record-btn:hover {
transform: scale(1.1);
}
.record-btn.recording {
background: #e74c3c;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
.record-status {
text-align: center;
margin-top: 15px;
color: #666;
font-size: 14px;
}
.record-timer {
text-align: center;
font-size: 24px;
font-weight: 700;
color: #667eea;
margin-top: 10px;
font-family: monospace;
}
.voice-text {
background: white;
border-radius: 10px;
padding: 20px;
margin-top: 20px;
text-align: center;
font-size: 18px;
font-weight: 600;
color: #333;
border: 2px dashed #667eea;
display: none;
}
.voice-text.show {
display: block;
}
.voice-preview {
margin-top: 15px;
display: none;
}
.voice-preview.show {
display: block;
}
.voice-preview audio {
width: 100%;
}
.voice-actions {
display: flex;
gap: 10px;
margin-top: 15px;
}
.voice-actions button {
flex: 1;
padding: 10px;
border-radius: 8px;
border: none;
cursor: pointer;
font-size: 14px;
font-weight: 600;
}
.btn-retry {
background: #e0e0e0;
color: #333;
}
.btn-use {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
/* File upload alternative */
.or-divider {
text-align: center;
margin: 20px 0;
color: #999;
font-size: 14px;
position: relative;
}
.or-divider::before,
.or-divider::after {
content: '';
position: absolute;
top: 50%;
width: 40%;
height: 1px;
background: #e0e0e0;
}
.or-divider::before { left: 0; }
.or-divider::after { right: 0; }
.file-input-wrapper {
border: 2px dashed #ccc;
border-radius: 10px;
padding: 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
}
.file-input-wrapper:hover {
border-color: #667eea;
background: #f8f9ff;
}
.file-input-wrapper.has-file {
border-color: #4caf50;
background: #f1f8f4;
}
input[type="file"] {
display: none;
}
.slider-group {
display: flex;
align-items: center;
gap: 15px;
}
input[type="range"] {
flex: 1;
}
.slider-value {
min-width: 50px;
text-align: center;
font-weight: 600;
color: #667eea;
}
button[type="submit"] {
width: 100%;
padding: 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
button[type="submit"]:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
}
button[type="submit"]:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.result {
margin-top: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 10px;
display: none;
}
.result.show {
display: block;
}
.result audio {
width: 100%;
margin-top: 15px;
}
.download-btn {
display: inline-block;
margin-top: 15px;
padding: 10px 20px;
background: #4caf50;
color: white;
text-decoration: none;
border-radius: 8px;
font-size: 14px;
}
.loading {
display: none;
text-align: center;
margin-top: 20px;
}
.loading.show {
display: block;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error {
color: #e74c3c;
background: #fdf2f2;
padding: 15px;
border-radius: 8px;
margin-top: 20px;
display: none;
}
.error.show {
display: block;
}
.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.check-badge {
display: inline-flex;
align-items: center;
gap: 5px;
background: #4caf50;
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
margin-left: 10px;
}
</style>
</head>
<body>
<div class="container">
<h1>🎙️ Chatterbox TTS</h1>
<form id="ttsForm">
<!-- Голосовая запись -->
<div class="voice-section" id="voiceSection">
<div class="voice-label">
🎤 Запишите голосовое сообщение для проверки на бота
<span class="check-badge" id="checkBadge" style="display: none;">✓ Готово</span>
</div>
<button type="button" class="record-btn" id="recordBtn"></button>
<div class="record-status" id="recordStatus">Нажмите для записи</div>
<div class="record-timer" id="recordTimer" style="display: none;">00:00</div>
<div class="voice-text" id="voiceText">
📢 Произнесите: <span style="color: #667eea;">"Хакатон 2026 французский стиль"</span>
</div>
<div class="voice-preview" id="voicePreview">
<audio id="recordedAudio" controls></audio>
<div class="voice-actions">
<button type="button" class="btn-retry" id="retryBtn">🔄 Перезаписать</button>
<button type="button" class="btn-use" id="useBtn">✅ Использовать</button>
</div>
</div>
</div>
<!-- Или загрузить файл -->
<div class="or-divider">или загрузите файл</div>
<div class="form-group">
<div class="file-input-wrapper" id="dropZone">
<input type="file" id="audioFile" accept="audio/*">
<div id="fileLabel">📁 Кликни или перетащи аудио файл</div>
<div id="fileName" style="margin-top: 10px; font-weight: 600; color: #667eea;"></div>
</div>
</div>
<!-- Текст для синтеза -->
<div class="form-group">
<label for="text">Текст для синтеза (макс 300 символов)</label>
<textarea id="text" maxlength="300" placeholder="Введите текст...">В прошлом месяце мы достигли нового рубежа.</textarea>
<div style="text-align: right; font-size: 12px; color: #999; margin-top: 5px;">
<span id="charCount">0</span>/300
</div>
</div>
<!-- Язык -->
<div class="form-group">
<label for="language">Язык</label>
<select id="language">
<option value="en">English</option>
<option value="ru" selected>Russian</option>
<option value="de">German</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
<option value="it">Italian</option>
<option value="pt">Portuguese</option>
<option value="hi">Hindi</option>
</select>
</div>
<!-- Слайдеры -->
<div class="form-group">
<label>Exaggeration (экспрессивность)</label>
<div class="slider-group">
<input type="range" id="exaggeration" min="0.25" max="2" step="0.05" value="0.5">
<span class="slider-value" id="exaggerationVal">0.5</span>
</div>
</div>
<div class="form-group">
<label>Temperature (разнообразие)</label>
<div class="slider-group">
<input type="range" id="temperature" min="0.05" max="5" step="0.05" value="0.8">
<span class="slider-value" id="temperatureVal">0.8</span>
</div>
</div>
<div class="form-group">
<label>CFG Weight</label>
<div class="slider-group">
<input type="range" id="cfgWeight" min="0.2" max="1" step="0.05" value="0.5">
<span class="slider-value" id="cfgWeightVal">0.5</span>
</div>
</div>
<div class="two-col">
<div class="form-group">
<label for="seed">Random Seed (0 = random)</label>
<input type="number" id="seed" value="0" min="0">
</div>
</div>
<button type="submit" id="submitBtn">🚀 Сгенерировать аудио</button>
</form>
<!-- Лоадер -->
<div class="loading" id="loading">
<div class="spinner"></div>
<p>Генерируем аудио... Это может занять 10-30 секунд</p>
</div>
<!-- Ошибка -->
<div class="error" id="error"></div>
<!-- Результат -->
<div class="result" id="result">
<h3 style="margin-bottom: 15px; color: #333;">✅ Готово!</h3>
<audio id="audioPlayer" controls></audio>
<br>
<a href="#" class="download-btn" id="downloadBtn" download="output.wav">💾 Скачать WAV</a>
</div>
</div>
<script>
const API_URL = 'https://back.hack.kinsle.ru/process-audio';
// Элементы
const form = document.getElementById('ttsForm');
const voiceSection = document.getElementById('voiceSection');
const recordBtn = document.getElementById('recordBtn');
const recordStatus = document.getElementById('recordStatus');
const recordTimer = document.getElementById('recordTimer');
const voiceText = document.getElementById('voiceText');
const voicePreview = document.getElementById('voicePreview');
const recordedAudio = document.getElementById('recordedAudio');
const retryBtn = document.getElementById('retryBtn');
const useBtn = document.getElementById('useBtn');
const checkBadge = document.getElementById('checkBadge');
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('audioFile');
const fileLabel = document.getElementById('fileLabel');
const fileName = document.getElementById('fileName');
const text = document.getElementById('text');
const charCount = document.getElementById('charCount');
const loading = document.getElementById('loading');
const result = document.getElementById('result');
const error = document.getElementById('error');
const audioPlayer = document.getElementById('audioPlayer');
const downloadBtn = document.getElementById('downloadBtn');
// Переменные для записи
let mediaRecorder = null;
let recordedChunks = [];
let recordingStartTime = null;
let recordingTimer = null;
let recordedBlob = null;
let isUsingRecordedVoice = false;
// Запрос разрешения на микрофон при загрузке
async function initMicrophone() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
stream.getTracks().forEach(track => track.stop());
} catch (err) {
console.log('Микрофон не доступен:', err);
}
}
initMicrophone();
// Начать/остановить запись
recordBtn.addEventListener('click', async () => {
if (mediaRecorder && mediaRecorder.state === 'recording') {
stopRecording();
} else {
startRecording();
}
});
async function startRecording() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const mimeType =
MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' :
MediaRecorder.isTypeSupported('audio/webm') ? 'audio/webm' :
MediaRecorder.isTypeSupported('audio/ogg;codecs=opus') ? 'audio/ogg;codecs=opus' : '';
mediaRecorder = new MediaRecorder(stream, mimeType ? { mimeType } : {});
recordedChunks = [];
mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) {
recordedChunks.push(e.data);
}
};
mediaRecorder.onstop = () => {
const actualType = mediaRecorder.mimeType || 'audio/webm';
recordedBlob = new Blob(recordedChunks, { type: actualType });
const url = URL.createObjectURL(recordedBlob);
recordedAudio.src = url;
voicePreview.classList.add('show');
recordStatus.textContent = 'Запись завершена';
recordBtn.textContent = '⏺';
recordBtn.classList.remove('recording');
voiceSection.classList.remove('recording');
voiceSection.classList.add('has-recording');
// Останавливаем все треки
stream.getTracks().forEach(track => track.stop());
};
mediaRecorder.start();
// UI обновления
recordBtn.textContent = '⏹';
recordBtn.classList.add('recording');
voiceSection.classList.add('recording');
recordStatus.textContent = 'Идёт запись...';
voiceText.classList.add('show');
recordTimer.style.display = 'block';
voicePreview.classList.remove('show');
isUsingRecordedVoice = false;
checkBadge.style.display = 'none';
// Таймер
recordingStartTime = Date.now();
updateTimer();
recordingTimer = setInterval(updateTimer, 1000);
} catch (err) {
alert('Не удалось получить доступ к микрофону: ' + err.message);
}
}
function stopRecording() {
if (mediaRecorder && mediaRecorder.state === 'recording') {
mediaRecorder.stop();
clearInterval(recordingTimer);
recordTimer.style.display = 'none';
voiceText.classList.remove('show');
}
}
function updateTimer() {
const elapsed = Math.floor((Date.now() - recordingStartTime) / 1000);
const mins = Math.floor(elapsed / 60).toString().padStart(2, '0');
const secs = (elapsed % 60).toString().padStart(2, '0');
recordTimer.textContent = `${mins}:${secs}`;
}
// Перезаписать
retryBtn.addEventListener('click', () => {
voicePreview.classList.remove('show');
voiceSection.classList.remove('has-recording');
recordStatus.textContent = 'Нажмите для записи';
recordedBlob = null;
isUsingRecordedVoice = false;
checkBadge.style.display = 'none';
});
// Использовать запись
useBtn.addEventListener('click', () => {
isUsingRecordedVoice = true;
checkBadge.style.display = 'inline-flex';
recordStatus.textContent = '✅ Голосовое сообщение сохранено';
// Сбрасываем файл если был
fileInput.value = '';
fileLabel.textContent = '📁 Кликни или перетащи аудио файл';
fileName.textContent = '';
dropZone.classList.remove('has-file');
});
// File upload (drag & drop)
dropZone.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.style.borderColor = '#667eea';
});
dropZone.addEventListener('dragleave', () => {
dropZone.style.borderColor = '#ccc';
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.style.borderColor = '#ccc';
const files = e.dataTransfer.files;
if (files.length) {
fileInput.files = files;
updateFileName(files[0].name);
}
});
fileInput.addEventListener('change', () => {
if (fileInput.files.length) {
updateFileName(fileInput.files[0].name);
// Сбрасываем голосовую запись
isUsingRecordedVoice = false;
checkBadge.style.display = 'none';
}
});
function updateFileName(name) {
fileLabel.textContent = '✅ Файл выбран';
fileName.textContent = name;
dropZone.classList.add('has-file');
}
// Character counter
text.addEventListener('input', () => {
charCount.textContent = text.value.length;
});
charCount.textContent = text.value.length;
// Slider updates
document.getElementById('exaggeration').addEventListener('input', (e) => {
document.getElementById('exaggerationVal').textContent = e.target.value;
});
document.getElementById('temperature').addEventListener('input', (e) => {
document.getElementById('temperatureVal').textContent = e.target.value;
});
document.getElementById('cfgWeight').addEventListener('input', (e) => {
document.getElementById('cfgWeightVal').textContent = e.target.value;
});
// Form submit
form.addEventListener('submit', async (e) => {
e.preventDefault();
// Проверяем что есть источник аудио
if (!isUsingRecordedVoice && !fileInput.files.length) {
alert('Пожалуйста, запишите голосовое сообщение или загрузите аудио файл');
return;
}
result.classList.remove('show');
error.classList.remove('show');
loading.classList.add('show');
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = true;
try {
const formData = new FormData();
// Добавляем аудио (голосовое или файл)
if (isUsingRecordedVoice && recordedBlob) {
const ext = recordedBlob.type.includes('ogg') ? 'ogg' : 'webm';
formData.append('audio_file', recordedBlob, `voice_recording.${ext}`);
} else if (fileInput.files.length > 0) {
formData.append('audio_file', fileInput.files[0]);
}
formData.append('text', text.value);
formData.append('language_id', document.getElementById('language').value);
formData.append('exaggeration', document.getElementById('exaggeration').value);
formData.append('temperature', document.getElementById('temperature').value);
formData.append('seed_num', document.getElementById('seed').value);
formData.append('cfg_weight', document.getElementById('cfgWeight').value);
const response = await fetch(API_URL, {
method: 'POST',
body: formData
});
if (!response.ok) {
const errData = await response.json().catch(() => ({}));
throw new Error(errData.error || `HTTP ${response.status}`);
}
const blob = await response.blob();
const url = URL.createObjectURL(blob);
audioPlayer.src = url;
downloadBtn.href = url;
result.classList.add('show');
} catch (err) {
error.textContent = '❌ Ошибка: ' + err.message;
error.classList.add('show');
console.error(err);
} finally {
loading.classList.remove('show');
submitBtn.disabled = false;
}
});
</script>
</body>
</html>