before stt adding

This commit is contained in:
2025-11-28 17:43:00 +06:00
parent fccafad6de
commit f933c315e8
17 changed files with 10002 additions and 672 deletions

Binary file not shown.

2152
static/fonts/PTSans-Bold.ttf Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,452 +1,517 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Go Speech - TTS</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
@font-face {
font-family: "Noto Color Emoji";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("/go-speech/fonts/NotoColorEmoji.ttf") format("truetype");
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
@font-face {
font-family: "PT Sans";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("/go-speech/fonts/PTSans-Regular.ttf") format("truetype");
}
.container {
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
padding: 40px;
max-width: 600px;
width: 100%;
}
@font-face {
font-family: "PT Sans";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("/go-speech/fonts/PTSans-Bold.ttf") format("truetype");
}
h1 {
color: #333;
margin-bottom: 30px;
text-align: center;
font-weight: 600;
font-size: 28px;
}
@font-face {
font-family: "PT Sans";
font-style: italic;
font-weight: 400;
font-display: swap;
src: url("/go-speech/fonts/PTSans-Italic.ttf") format("truetype");
}
.form-group {
margin-bottom: 24px;
}
@font-face {
font-family: "PT Sans";
font-style: italic;
font-weight: 700;
font-display: swap;
src: url("/go-speech/fonts/PTSans-BoldItalic.ttf") format("truetype");
}
label {
display: block;
margin-bottom: 8px;
color: #555;
font-weight: 500;
font-size: 14px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
select {
width: 100%;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
background: white;
color: #333;
cursor: pointer;
transition: border-color 0.3s;
}
body {
font-family: "PT Sans", -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
select:focus {
outline: none;
border-color: #667eea;
}
.container {
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
padding: 40px;
max-width: 600px;
width: 100%;
}
textarea {
width: 100%;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
font-family: inherit;
resize: vertical;
min-height: 120px;
color: #333;
transition: border-color 0.3s;
}
h1 {
color: #333;
margin-bottom: 30px;
text-align: center;
font-weight: 600;
font-size: 28px;
font-family: "Noto Color Emoji", -apple-system, BlinkMacSystemFont,
"Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
}
textarea:focus {
outline: none;
border-color: #667eea;
}
.form-group {
margin-bottom: 24px;
}
.buttons {
display: flex;
gap: 12px;
margin-top: 24px;
}
label {
display: block;
margin-bottom: 8px;
color: #555;
font-weight: 700;
font-size: 16px;
font-family: "PT Sans", sans-serif;
}
button {
flex: 1;
padding: 14px 24px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
text-transform: uppercase;
letter-spacing: 0.5px;
}
select {
width: 100%;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
font-family: "PT Sans", sans-serif;
background: white;
color: #333;
cursor: pointer;
transition: border-color 0.3s;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
select:focus {
outline: none;
border-color: #667eea;
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
}
textarea {
width: 100%;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
font-family: "PT Sans", sans-serif;
resize: vertical;
min-height: 120px;
color: #333;
transition: border-color 0.3s;
}
.btn-secondary {
background: #f5f5f5;
color: #333;
}
textarea:focus {
outline: none;
border-color: #667eea;
}
.btn-secondary:hover:not(:disabled) {
background: #e8e8e8;
}
.buttons {
display: flex;
gap: 12px;
margin-top: 24px;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
button {
flex: 1;
padding: 14px 24px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
text-transform: uppercase;
letter-spacing: 0.5px;
font-family: "Noto Color Emoji", "PT Sans", -apple-system,
BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell,
sans-serif;
}
.audio-player {
margin-top: 24px;
display: none;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.audio-player.show {
display: block;
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
}
audio {
width: 100%;
margin-top: 12px;
}
.btn-secondary {
background: #f5f5f5;
color: #333;
}
.status {
margin-top: 16px;
padding: 12px;
border-radius: 8px;
font-size: 14px;
text-align: center;
display: none;
}
.btn-secondary:hover:not(:disabled) {
background: #e8e8e8;
}
.status.show {
display: block;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.status.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.audio-player {
margin-top: 24px;
display: none;
}
.status.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.audio-player.show {
display: block;
}
.status.loading {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
audio {
width: 100%;
margin-top: 12px;
}
.loader-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.95);
display: none;
align-items: center;
justify-content: center;
border-radius: 16px;
z-index: 1000;
}
.status {
margin-top: 16px;
padding: 12px;
border-radius: 8px;
font-size: 14px;
font-family: "PT Sans", sans-serif;
text-align: center;
display: none;
}
.loader-overlay.show {
display: flex;
}
.status.show {
display: block;
}
.loader {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.status.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.status.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.status.loading {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.loader-text {
color: #667eea;
font-weight: 500;
font-size: 16px;
}
.loader-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.95);
display: none;
align-items: center;
justify-content: center;
border-radius: 16px;
z-index: 1000;
}
.container {
position: relative;
}
.loader-overlay.show {
display: flex;
}
.form-disabled {
pointer-events: none;
opacity: 0.6;
.loader {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.loader-text {
color: #667eea;
font-weight: 500;
font-size: 16px;
font-family: "PT Sans", sans-serif;
}
.container {
position: relative;
}
.form-disabled {
pointer-events: none;
opacity: 0.6;
}
.version-info {
text-align: center;
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid #e0e0e0;
}
</style>
</head>
<body>
</head>
<body>
<div class="container">
<div class="loader-overlay" id="loaderOverlay">
<div class="loader">
<div class="spinner"></div>
<div class="loader-text" id="loaderText">Генерация аудио...</div>
</div>
<div class="loader-overlay" id="loaderOverlay">
<div class="loader">
<div class="spinner"></div>
<div class="loader-text" id="loaderText">Генерация аудио...</div>
</div>
</div>
<h1>🎤 Go Speech TTS</h1>
<form id="ttsForm">
<div class="form-group">
<label for="voice">Выберите голос:</label>
<select id="voice" name="voice">
<option value="ruslan">Ruslan (мужской)</option>
<option value="irina">Irina (женский)</option>
<option value="denis">Denis (мужской)</option>
<option value="dmitri">Dmitri (мужской)</option>
</select>
</div>
<h1>🎤 Go Speech TTS</h1>
<form id="ttsForm">
<div class="form-group">
<label for="voice">Выберите голос:</label>
<select id="voice" name="voice">
<option value="ruslan">Ruslan (мужской)</option>
<option value="irina">Irina (женский)</option>
<option value="denis">Denis (мужской)</option>
<option value="dmitri">Dmitri (мужской)</option>
</select>
</div>
<div class="form-group">
<label for="text">Введите текст для озвучки:</label>
<textarea
id="text"
name="text"
placeholder="Введите текст на русском языке..."
required
></textarea>
</div>
<div class="buttons">
<button type="submit" class="btn-primary" id="speakBtn">
🔊 Озвучить
</button>
<button type="button" class="btn-secondary" id="downloadBtn" disabled>
💾 Скачать
</button>
</div>
</form>
<div class="status" id="status"></div>
<div class="audio-player" id="audioPlayer">
<audio id="audio" controls></audio>
<div class="form-group">
<label for="text">Введите текст для озвучки:</label>
<textarea
id="text"
name="text"
placeholder="Введите текст на русском языке..."
required
></textarea>
</div>
<div class="buttons">
<button type="submit" class="btn-primary" id="speakBtn">
🔊 Озвучить
</button>
<button type="button" class="btn-secondary" id="downloadBtn" disabled>
💾 Скачать
</button>
</div>
</form>
<div class="version-info">
<span style="font-size: 0.85em; color: #999; font-weight: normal"
>версия: {{VERSION}}</span
>
</div>
<div class="status" id="status"></div>
<div class="audio-player" id="audioPlayer">
<audio id="audio" controls></audio>
</div>
</div>
<script>
const form = document.getElementById('ttsForm');
const textInput = document.getElementById('text');
const voiceSelect = document.getElementById('voice');
const speakBtn = document.getElementById('speakBtn');
const downloadBtn = document.getElementById('downloadBtn');
const statusDiv = document.getElementById('status');
const audioPlayer = document.getElementById('audioPlayer');
const audio = document.getElementById('audio');
const loaderOverlay = document.getElementById('loaderOverlay');
const loaderText = document.getElementById('loaderText');
const container = document.querySelector('.container');
let currentAudioUrl = null;
let currentBlob = null;
let isPlaying = false;
const form = document.getElementById("ttsForm");
const textInput = document.getElementById("text");
const voiceSelect = document.getElementById("voice");
const speakBtn = document.getElementById("speakBtn");
const downloadBtn = document.getElementById("downloadBtn");
const statusDiv = document.getElementById("status");
const audioPlayer = document.getElementById("audioPlayer");
const audio = document.getElementById("audio");
const loaderOverlay = document.getElementById("loaderOverlay");
const loaderText = document.getElementById("loaderText");
const container = document.querySelector(".container");
function showStatus(message, type) {
statusDiv.textContent = message;
statusDiv.className = `status show ${type}`;
let currentAudioUrl = null;
let currentBlob = null;
let isPlaying = false;
function showStatus(message, type) {
statusDiv.textContent = message;
statusDiv.className = `status show ${type}`;
}
function hideStatus() {
statusDiv.className = "status";
}
function setLoading(loading, message = "Генерация аудио...") {
speakBtn.disabled = loading;
downloadBtn.disabled = loading || isPlaying;
if (loading) {
speakBtn.textContent = "⏳ Обработка...";
loaderText.textContent = message;
loaderOverlay.classList.add("show");
container.classList.add("form-disabled");
} else {
speakBtn.textContent = "🔊 Озвучить";
loaderOverlay.classList.remove("show");
container.classList.remove("form-disabled");
updateButtonsState();
}
}
function hideStatus() {
statusDiv.className = 'status';
}
function updateButtonsState() {
// Блокируем кнопки во время воспроизведения
speakBtn.disabled = isPlaying;
downloadBtn.disabled = isPlaying || !currentBlob;
}
function setLoading(loading, message = 'Генерация аудио...') {
speakBtn.disabled = loading;
downloadBtn.disabled = loading || isPlaying;
if (loading) {
speakBtn.textContent = '⏳ Обработка...';
loaderText.textContent = message;
loaderOverlay.classList.add('show');
container.classList.add('form-disabled');
} else {
speakBtn.textContent = '🔊 Озвучить';
loaderOverlay.classList.remove('show');
container.classList.remove('form-disabled');
updateButtonsState();
}
}
async function generateSpeech(text, voice) {
try {
setLoading(true);
hideStatus();
showStatus("Генерация аудио...", "loading");
function updateButtonsState() {
// Блокируем кнопки во время воспроизведения
speakBtn.disabled = isPlaying;
downloadBtn.disabled = isPlaying || !currentBlob;
}
const response = await fetch("/go-speech/api/v1/tts", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ text: text, voice: voice }),
});
async function generateSpeech(text, voice) {
try {
setLoading(true);
hideStatus();
showStatus('Генерация аудио...', 'loading');
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText || "Ошибка генерации аудио");
}
const response = await fetch('/go-speech/api/v1/tts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ text: text, voice: voice })
});
const blob = await response.blob();
currentBlob = blob;
currentAudioUrl = URL.createObjectURL(blob);
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText || 'Ошибка генерации аудио');
}
audio.src = currentAudioUrl;
audioPlayer.classList.add("show");
const blob = await response.blob();
currentBlob = blob;
currentAudioUrl = URL.createObjectURL(blob);
audio.src = currentAudioUrl;
audioPlayer.classList.add('show');
showStatus('Аудио готово!', 'success');
// Устанавливаем флаг воспроизведения перед автоплеем
isPlaying = true;
updateButtonsState();
// Автоматическое воспроизведение
audio.play().catch(err => {
console.log('Автовоспроизведение заблокировано:', err);
isPlaying = false;
updateButtonsState();
});
showStatus("Аудио готово!", "success");
} catch (error) {
console.error('Ошибка:', error);
showStatus('Ошибка: ' + error.message, 'error');
audioPlayer.classList.remove('show');
currentBlob = null;
isPlaying = false;
} finally {
setLoading(false);
}
}
// Устанавливаем флаг воспроизведения перед автоплеем
isPlaying = true;
updateButtonsState();
function downloadAudio() {
if (!currentBlob) {
showStatus('Нет аудио для скачивания', 'error');
return;
}
const url = currentAudioUrl;
const a = document.createElement('a');
a.href = url;
a.download = `speech-${voiceSelect.value}-${Date.now()}.ogg`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
showStatus('Файл скачан', 'success');
setTimeout(hideStatus, 2000);
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
const text = textInput.value.trim();
if (!text) {
showStatus('Введите текст для озвучки', 'error');
return;
}
if (text.length > 5000) {
showStatus('Текст слишком длинный (максимум 5000 символов)', 'error');
return;
}
const voice = voiceSelect.value;
await generateSpeech(text, voice);
});
downloadBtn.addEventListener('click', downloadAudio);
// Отслеживание событий воспроизведения аудио
audio.addEventListener('play', () => {
isPlaying = true;
updateButtonsState();
showStatus('Воспроизведение...', 'loading');
});
audio.addEventListener('pause', () => {
// Автоматическое воспроизведение
audio.play().catch((err) => {
console.log("Автовоспроизведение заблокировано:", err);
isPlaying = false;
updateButtonsState();
hideStatus();
});
});
} catch (error) {
console.error("Ошибка:", error);
showStatus("Ошибка: " + error.message, "error");
audioPlayer.classList.remove("show");
currentBlob = null;
isPlaying = false;
} finally {
setLoading(false);
}
}
audio.addEventListener('ended', () => {
isPlaying = false;
updateButtonsState();
showStatus('Воспроизведение завершено', 'success');
setTimeout(hideStatus, 2000);
});
function downloadAudio() {
if (!currentBlob) {
showStatus("Нет аудио для скачивания", "error");
return;
}
audio.addEventListener('error', () => {
isPlaying = false;
updateButtonsState();
showStatus('Ошибка воспроизведения', 'error');
});
const url = currentAudioUrl;
const a = document.createElement("a");
a.href = url;
a.download = `speech-${voiceSelect.value}-${Date.now()}.ogg`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
// Очистка URL при разгрузке страницы
window.addEventListener('beforeunload', () => {
if (currentAudioUrl) {
URL.revokeObjectURL(currentAudioUrl);
}
});
showStatus("Файл скачан", "success");
setTimeout(hideStatus, 2000);
}
form.addEventListener("submit", async (e) => {
e.preventDefault();
const text = textInput.value.trim();
if (!text) {
showStatus("Введите текст для озвучки", "error");
return;
}
if (text.length > 5000) {
showStatus("Текст слишком длинный (максимум 5000 символов)", "error");
return;
}
const voice = voiceSelect.value;
await generateSpeech(text, voice);
});
downloadBtn.addEventListener("click", downloadAudio);
// Отслеживание событий воспроизведения аудио
audio.addEventListener("play", () => {
isPlaying = true;
updateButtonsState();
showStatus("Воспроизведение...", "loading");
});
audio.addEventListener("pause", () => {
isPlaying = false;
updateButtonsState();
hideStatus();
});
audio.addEventListener("ended", () => {
isPlaying = false;
updateButtonsState();
showStatus("Воспроизведение завершено", "success");
setTimeout(hideStatus, 2000);
});
audio.addEventListener("error", () => {
isPlaying = false;
updateButtonsState();
showStatus("Ошибка воспроизведения", "error");
});
// Очистка URL при разгрузке страницы
window.addEventListener("beforeunload", () => {
if (currentAudioUrl) {
URL.revokeObjectURL(currentAudioUrl);
}
});
</script>
</body>
</body>
</html>