VOX33 / static /index.html
ssasio's picture
Upload static/index.html
8a298ea verified
<!DOCTYPE html>
<html lang="bg">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🎙️ VOX ANI TTS</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0f0f13;
--bg2: #17171e;
--bg3: #1e1e28;
--border: #2e2e3e;
--text: #e2e2ec;
--muted: #8888aa;
--accent: #7c6ef5;
--accent2: #5b4fd4;
--success: #4caf7d;
--danger: #e05555;
--warn: #e09030;
--radius: 10px;
--font: 'Segoe UI', system-ui, sans-serif;
}
body {
font-family: var(--font);
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 24px 16px 60px;
}
h1 {
font-size: 1.7rem;
font-weight: 700;
letter-spacing: -0.5px;
margin-bottom: 4px;
background: linear-gradient(135deg, #a89af8, #7c6ef5);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
color: var(--muted);
font-size: .875rem;
margin-bottom: 24px;
}
.card {
background: var(--bg2);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
width: 100%;
max-width: 780px;
}
/* Key row */
.key-row {
display: flex;
gap: 10px;
margin-bottom: 20px;
max-width: 780px;
width: 100%;
}
.key-row input {
flex: 1;
}
/* Tabs */
.tabs {
display: flex;
gap: 4px;
margin-bottom: 20px;
max-width: 780px;
width: 100%;
}
.tab-btn {
flex: 1;
padding: 9px 6px;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--bg2);
color: var(--muted);
cursor: pointer;
font-size: .82rem;
font-family: var(--font);
transition: all .15s;
}
.tab-btn.active {
background: var(--accent);
border-color: var(--accent);
color: #fff;
font-weight: 600;
}
.tab-btn:hover:not(.active) {
border-color: var(--accent);
color: var(--text);
}
.tab-panel { display: none; }
.tab-panel.active { display: block; }
/* Form elements */
label {
display: block;
font-size: .82rem;
color: var(--muted);
margin-bottom: 5px;
margin-top: 14px;
}
label:first-child { margin-top: 0; }
input[type=text],
input[type=password],
input[type=number],
select,
textarea {
width: 100%;
background: var(--bg3);
border: 1px solid var(--border);
border-radius: 7px;
color: var(--text);
font-family: var(--font);
font-size: .92rem;
padding: 9px 12px;
outline: none;
transition: border .15s;
}
input:focus, select:focus, textarea:focus {
border-color: var(--accent);
}
textarea { resize: vertical; min-height: 90px; }
.row { display: flex; gap: 12px; }
.row > * { flex: 1; }
/* Range */
.slider-row {
display: flex;
align-items: center;
gap: 10px;
}
.slider-row input[type=range] {
flex: 1;
accent-color: var(--accent);
padding: 0;
height: 4px;
cursor: pointer;
}
.slider-row .val {
min-width: 36px;
text-align: right;
font-size: .82rem;
color: var(--muted);
}
/* Accordion */
details {
border: 1px solid var(--border);
border-radius: var(--radius);
margin-top: 14px;
overflow: hidden;
}
summary {
padding: 11px 14px;
cursor: pointer;
font-size: .88rem;
color: var(--muted);
list-style: none;
display: flex;
align-items: center;
gap: 6px;
user-select: none;
}
summary::before { content: '▶'; font-size: .7rem; transition: transform .15s; }
details[open] summary::before { transform: rotate(90deg); }
.accordion-body {
padding: 0 14px 14px;
}
/* Buttons */
button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
border: none;
border-radius: 7px;
cursor: pointer;
font-family: var(--font);
font-size: .9rem;
font-weight: 600;
padding: 10px 18px;
transition: opacity .15s, transform .1s;
}
button:active { transform: scale(.97); }
button:disabled { opacity: .45; cursor: not-allowed; }
.btn-primary { background: var(--accent); color: #fff; }
.btn-primary:hover:not(:disabled) { background: var(--accent2); }
.btn-danger { background: var(--danger); color: #fff; }
.btn-danger:hover:not(:disabled) { opacity: .85; }
.btn-secondary { background: var(--bg3); border: 1px solid var(--border); color: var(--text); }
.btn-secondary:hover:not(:disabled) { border-color: var(--accent); }
.btn-row { display: flex; gap: 10px; margin-top: 16px; flex-wrap: wrap; }
/* Status */
.status {
margin-top: 12px;
padding: 9px 13px;
border-radius: 7px;
font-size: .88rem;
background: var(--bg3);
border: 1px solid var(--border);
color: var(--muted);
min-height: 38px;
display: flex;
align-items: center;
gap: 6px;
}
.status.ok { border-color: var(--success); color: var(--success); }
.status.err { border-color: var(--danger); color: var(--danger); }
.status.warn { border-color: var(--warn); color: var(--warn); }
/* Audio player */
audio {
width: 100%;
margin-top: 12px;
border-radius: 7px;
accent-color: var(--accent);
}
/* Voices list */
#voices-list, #manage-list {
background: var(--bg3);
border: 1px solid var(--border);
border-radius: 7px;
padding: 10px 12px;
min-height: 60px;
font-size: .85rem;
line-height: 1.8;
color: var(--muted);
margin-top: 6px;
white-space: pre-line;
}
/* Divider */
hr { border: none; border-top: 1px solid var(--border); margin: 20px 0; }
/* Checkbox */
.check-row {
display: flex;
align-items: center;
gap: 8px;
margin-top: 14px;
}
.check-row input[type=checkbox] {
width: 16px;
height: 16px;
accent-color: var(--accent);
cursor: pointer;
}
.check-row label {
margin: 0;
font-size: .9rem;
color: var(--text);
cursor: pointer;
}
.section-title {
font-weight: 600;
font-size: .95rem;
color: var(--text);
margin-bottom: 10px;
}
/* Spinner */
@keyframes spin { to { transform: rotate(360deg); } }
.spin { display: inline-block; animation: spin .7s linear infinite; }
/* Download link */
.dl-link {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--accent);
font-size: .88rem;
text-decoration: none;
margin-top: 8px;
}
.dl-link:hover { text-decoration: underline; }
</style>
</head>
<body>
<h1>🎙️ VOX ANI TTS</h1>
<p class="subtitle">Neural text-to-speech with voice cloning</p>
<!-- API Key -->
<div class="key-row">
<input type="password" id="api-key" placeholder="🔑 API Key" oninput="onKeyChange()">
</div>
<!-- Tabs -->
<div class="tabs">
<button class="tab-btn active" onclick="showTab('synth')">🔊 Синтез</button>
<button class="tab-btn" onclick="showTab('clone')">🎤 Клониране</button>
<button class="tab-btn" onclick="showTab('manage')">🗑️ Управление</button>
<button class="tab-btn" onclick="showTab('encode')">🔐 Encode Key</button>
</div>
<!-- ───────────── TAB 1: SYNTHESIZE ───────────── -->
<div class="card tab-panel active" id="tab-synth">
<label for="synth-text">Текст за синтезиране</label>
<textarea id="synth-text" placeholder="Въведете текст…"></textarea>
<label for="voice-select">Глас</label>
<select id="voice-select"><option value="">— зарежда се —</option></select>
<label>Reference аудио (по избор — замества избрания глас)</label>
<input type="file" id="ref-audio-synth" accept="audio/*" style="color:var(--muted);font-size:.85rem">
<details>
<summary>⚙️ Параметри на генерацията</summary>
<div class="accordion-body">
<label>Temperature</label>
<div class="slider-row">
<input type="range" min="0.05" max="1.0" step="0.05" value="0.3" id="temperature"
oninput="document.getElementById('temperature-val').textContent=this.value">
<span class="val" id="temperature-val">0.3</span>
</div>
<label>Top-K</label>
<div class="slider-row">
<input type="range" min="10" max="500" step="10" value="250" id="top-k"
oninput="document.getElementById('top-k-val').textContent=this.value">
<span class="val" id="top-k-val">250</span>
</div>
<label>Top-P</label>
<div class="slider-row">
<input type="range" min="0.5" max="1.0" step="0.05" value="0.95" id="top-p"
oninput="document.getElementById('top-p-val').textContent=this.value">
<span class="val" id="top-p-val">0.95</span>
</div>
</div>
</details>
<div class="btn-row">
<button class="btn-primary" onclick="synthesize()" id="synth-btn">🔊 Генерирай</button>
</div>
<div class="status" id="synth-status">Готов</div>
<audio id="synth-audio" controls style="display:none"></audio>
</div>
<!-- ───────────── TAB 2: CLONE ───────────── -->
<div class="card tab-panel" id="tab-clone">
<label for="voice-name">Име на гласа</label>
<input type="text" id="voice-name" placeholder="напр. Иван">
<label>Reference аудио (WAV / MP3, мин. 5 сек.)</label>
<input type="file" id="ref-audio-clone" accept="audio/*" style="color:var(--muted);font-size:.85rem">
<details open>
<summary>🎛️ Почистване на аудио (препоръчително)</summary>
<div class="accordion-body">
<div class="check-row">
<input type="checkbox" id="do-enhance" checked onchange="toggleEnhance()">
<label for="do-enhance">Активирай почистване на гласа</label>
</div>
<div id="enhance-sliders">
<label>🔇 Noise reduction</label>
<div class="slider-row">
<input type="range" min="0" max="1" step="0.05" value="0.75" id="denoise"
oninput="document.getElementById('denoise-val').textContent=this.value">
<span class="val" id="denoise-val">0.75</span>
</div>
<label>🐍 De-essing (dB)</label>
<div class="slider-row">
<input type="range" min="0" max="12" step="0.5" value="6" id="deess"
oninput="document.getElementById('deess-val').textContent=this.value">
<span class="val" id="deess-val">6</span>
</div>
<label>🔥 Warming (dB)</label>
<div class="slider-row">
<input type="range" min="0" max="6" step="0.5" value="2.5" id="warm"
oninput="document.getElementById('warm-val').textContent=this.value">
<span class="val" id="warm-val">2.5</span>
</div>
</div>
</div>
</details>
<div class="btn-row">
<button class="btn-primary" onclick="cloneVoice()" id="clone-btn">💾 Запази гласа</button>
</div>
<div class="status" id="clone-status">Готов</div>
<label style="margin-top:16px">Запазени гласове</label>
<div id="voices-list"></div>
</div>
<!-- ───────────── TAB 3: MANAGE ───────────── -->
<div class="card tab-panel" id="tab-manage">
<p class="section-title">💾 Свали глас като JSON</p>
<label for="dl-voice">Избери глас</label>
<select id="dl-voice"><option value="">— зарежда се —</option></select>
<div class="btn-row">
<button class="btn-secondary" onclick="downloadVoice()" id="dl-btn">⬇️ Свали JSON</button>
</div>
<div class="status" id="dl-status"></div>
<hr>
<p class="section-title">🗑️ Изтрий глас</p>
<label for="del-voice">Избери глас за изтриване</label>
<select id="del-voice"><option value="">— зарежда се —</option></select>
<div class="btn-row">
<button class="btn-danger" onclick="deleteVoice()" id="del-btn">🗑️ Изтрий</button>
</div>
<div class="status" id="del-status"></div>
<label style="margin-top:16px">Текущ списък</label>
<div id="manage-list"></div>
</div>
<!-- ───────────── TAB 4: ENCODE KEY ───────────── -->
<div class="card tab-panel" id="tab-encode">
<p class="section-title">🔒 Encode API Key</p>
<label for="raw-key">Реален ключ</label>
<input type="password" id="raw-key" placeholder="Въведи реалния API ключ">
<div class="btn-row">
<button class="btn-primary" onclick="encodeKey()">🔒 Encode</button>
</div>
<label style="margin-top:16px">Кодиран ключ (за app.py)</label>
<input type="text" id="encoded-out" readonly placeholder="резултатът ще се появи тук" onclick="this.select()">
<hr>
<p class="section-title">🔓 Decode API Key</p>
<label for="encoded-key">Кодиран ключ</label>
<input type="text" id="encoded-key" placeholder="Постави кодирания ключ">
<div class="btn-row">
<button class="btn-secondary" onclick="decodeKey()">🔓 Decode</button>
</div>
<label style="margin-top:16px">Декодиран ключ</label>
<input type="text" id="decoded-out" readonly placeholder="резултатът ще се появи тук" onclick="this.select()">
</div>
<script src="/static/app.js"></script>
</body>
</html>