| <!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 { |
| display: flex; |
| gap: 10px; |
| margin-bottom: 20px; |
| max-width: 780px; |
| width: 100%; |
| } |
| .key-row input { |
| flex: 1; |
| } |
| |
| |
| .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; } |
| |
| |
| 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; } |
| |
| |
| .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); |
| } |
| |
| |
| 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; |
| } |
| |
| |
| 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 { |
| 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 { |
| width: 100%; |
| margin-top: 12px; |
| border-radius: 7px; |
| accent-color: var(--accent); |
| } |
| |
| |
| #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; |
| } |
| |
| |
| hr { border: none; border-top: 1px solid var(--border); margin: 20px 0; } |
| |
| |
| .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; |
| } |
| |
| |
| @keyframes spin { to { transform: rotate(360deg); } } |
| .spin { display: inline-block; animation: spin .7s linear infinite; } |
| |
| |
| .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> |
|
|
| |
| <div class="key-row"> |
| <input type="password" id="api-key" placeholder="🔑 API Key" oninput="onKeyChange()"> |
| </div> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|