/* ── VOX ANI TTS — Frontend Logic ── */ // ─── Helpers ──────────────────────────────────────────────── function apiKey() { return document.getElementById("api-key").value.trim(); } function setStatus(id, msg, type = "") { const el = document.getElementById(id); el.textContent = msg; el.className = "status" + (type ? " " + type : ""); } function setLoading(btnId, loading, label) { const btn = document.getElementById(btnId); btn.disabled = loading; btn.innerHTML = loading ? ` ${label}` : btn.dataset.label; } function initBtn(btnId, label) { const btn = document.getElementById(btnId); btn.dataset.label = label; } // ─── Tabs ─────────────────────────────────────────────────── function showTab(name) { document.querySelectorAll(".tab-panel").forEach((p) => p.classList.remove("active")); document.querySelectorAll(".tab-btn").forEach((b) => b.classList.remove("active")); document.getElementById("tab-" + name).classList.add("active"); const map = { synth: 0, clone: 1, manage: 2, encode: 3 }; document.querySelectorAll(".tab-btn")[map[name]].classList.add("active"); } // ─── Load voices ───────────────────────────────────────────── async function loadVoices() { const key = apiKey(); if (!key) return; try { const res = await fetch(`/voices?api_key=${encodeURIComponent(key)}`); if (!res.ok) { clearVoiceDropdowns(); return; } const data = await res.json(); const voices = data.voices || []; populateDropdown("voice-select", voices, true); const cloned = voices.filter((v) => v.type === "cloned"); populateDropdown("dl-voice", cloned, false); populateDropdown("del-voice", cloned, false); renderVoiceList(cloned); } catch { clearVoiceDropdowns(); } } function populateDropdown(id, voices, includeAll) { const el = document.getElementById(id); el.innerHTML = includeAll ? voices.map((v) => ``).join("") : `` + voices.map((v) => ``).join(""); } function clearVoiceDropdowns() { ["voice-select", "dl-voice", "del-voice"].forEach((id) => { document.getElementById(id).innerHTML = ``; }); ["voices-list", "manage-list"].forEach((id) => { document.getElementById(id).textContent = "🔐 Въведи валиден API ключ"; }); } function renderVoiceList(cloned) { const text = cloned.length ? cloned.map((v) => `• ${v.name} (ID: ${v.id})`).join("\n") : "Няма запазени гласове"; document.getElementById("voices-list").textContent = text; document.getElementById("manage-list").textContent = text; } function onKeyChange() { clearTimeout(window._keyTimer); window._keyTimer = setTimeout(loadVoices, 500); } // ─── Enhance toggle ────────────────────────────────────────── function toggleEnhance() { const on = document.getElementById("do-enhance").checked; document.getElementById("enhance-sliders").style.opacity = on ? "1" : "0.4"; document.getElementById("enhance-sliders").style.pointerEvents = on ? "" : "none"; } // ─── Synthesize ────────────────────────────────────────────── async function synthesize() { const key = apiKey(); const text = document.getElementById("synth-text").value.trim(); const voice = document.getElementById("voice-select").value; const refFile = document.getElementById("ref-audio-synth").files[0]; if (!key) return setStatus("synth-status", "⚠️ Въведи API ключ", "warn"); if (!text) return setStatus("synth-status", "⚠️ Въведи текст", "warn"); setStatus("synth-status", "⟳ Генериране…"); document.getElementById("synth-btn").disabled = true; try { let url; if (refFile) { // Encode reference audio → embedding → synthesize with embedding setStatus("synth-status", "⟳ Encode на reference аудио…"); const form = new FormData(); form.append("file", refFile); const encRes = await fetch( `/encode_voice?api_key=${encodeURIComponent(key)}&enhance=false`, { method: "POST", body: form } ); if (!encRes.ok) throw new Error(await encRes.text()); const { embedding } = await encRes.json(); setStatus("synth-status", "⟳ Синтез…"); url = `/synthesize_with_embedding?api_key=${encodeURIComponent(key)}` + `&text=${encodeURIComponent(text)}` + `&embedding=${encodeURIComponent(JSON.stringify(embedding))}`; } else { if (!voice) return setStatus("synth-status", "⚠️ Избери глас", "warn"); url = `/synthesize?api_key=${encodeURIComponent(key)}` + `&text=${encodeURIComponent(text)}` + `&voice=${encodeURIComponent(voice)}`; } const res = await fetch(url); if (!res.ok) throw new Error(await res.text()); const blob = await res.blob(); const audioEl = document.getElementById("synth-audio"); audioEl.src = URL.createObjectURL(blob); audioEl.style.display = "block"; audioEl.play(); setStatus("synth-status", "✅ Готово", "ok"); } catch (e) { setStatus("synth-status", "❌ Грешка: " + e.message, "err"); } finally { document.getElementById("synth-btn").disabled = false; } } // ─── Clone voice ───────────────────────────────────────────── async function cloneVoice() { const key = apiKey(); const name = document.getElementById("voice-name").value.trim(); const fileEl = document.getElementById("ref-audio-clone"); const file = fileEl.files[0]; const enhance = document.getElementById("do-enhance").checked; const denoise = document.getElementById("denoise").value; const deess = document.getElementById("deess").value; const warm = document.getElementById("warm").value; if (!key) return setStatus("clone-status", "⚠️ Въведи API ключ", "warn"); if (!file) return setStatus("clone-status", "⚠️ Качи аудио файл", "warn"); setStatus("clone-status", "⟳ Клониране…"); document.getElementById("clone-btn").disabled = true; try { const form = new FormData(); form.append("file", file); const params = new URLSearchParams({ api_key: key, name: name || "", enhance: enhance, denoise_strength: denoise, deess_db: deess, warm_db: warm, }); const res = await fetch(`/clone_voice?${params}`, { method: "POST", body: form, }); if (!res.ok) throw new Error(await res.text()); const data = await res.json(); setStatus("clone-status", `✅ Гласът '${data.name}' е запазен!`, "ok"); fileEl.value = ""; document.getElementById("voice-name").value = ""; await loadVoices(); } catch (e) { setStatus("clone-status", "❌ Грешка: " + e.message, "err"); } finally { document.getElementById("clone-btn").disabled = false; } } // ─── Download voice ────────────────────────────────────────── async function downloadVoice() { const key = apiKey(); const voiceId = document.getElementById("dl-voice").value; if (!key) return setStatus("dl-status", "⚠️ Въведи API ключ", "warn"); if (!voiceId) return setStatus("dl-status", "⚠️ Избери глас", "warn"); setStatus("dl-status", "⟳ Изтегляне…"); try { const res = await fetch( `/voices/${encodeURIComponent(voiceId)}/download?api_key=${encodeURIComponent(key)}` ); if (!res.ok) throw new Error(await res.text()); const blob = await res.blob(); const cd = res.headers.get("content-disposition") || ""; const match = cd.match(/filename="?([^"]+)"?/); const fname = match ? match[1] : `voice_${voiceId}.json`; const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = fname; a.click(); setStatus("dl-status", `✅ ${fname} изтеглен`, "ok"); } catch (e) { setStatus("dl-status", "❌ Грешка: " + e.message, "err"); } } // ─── Delete voice ──────────────────────────────────────────── async function deleteVoice() { const key = apiKey(); const voiceId = document.getElementById("del-voice").value; if (!key) return setStatus("del-status", "⚠️ Въведи API ключ", "warn"); if (!voiceId) return setStatus("del-status", "⚠️ Избери глас", "warn"); if (!confirm("Сигурен ли си, че искаш да изтриеш този глас?")) return; setStatus("del-status", "⟳ Изтриване…"); document.getElementById("del-btn").disabled = true; try { const res = await fetch( `/voices/${encodeURIComponent(voiceId)}?api_key=${encodeURIComponent(key)}`, { method: "DELETE" } ); if (!res.ok) throw new Error(await res.text()); const data = await res.json(); setStatus("del-status", `✅ '${data.name}' е изтрит`, "ok"); await loadVoices(); } catch (e) { setStatus("del-status", "❌ Грешка: " + e.message, "err"); } finally { document.getElementById("del-btn").disabled = false; } } // ─── Encode / Decode API Key ──────────────────────────────── function encodeKey() { const key = document.getElementById("raw-key").value.trim(); if (!key) return; const encoded = btoa(key).split("").reverse().join(""); document.getElementById("encoded-out").value = encoded; } function decodeKey() { const encoded = document.getElementById("encoded-key").value.trim(); if (!encoded) return; try { const decoded = atob(encoded.split("").reverse().join("")); document.getElementById("decoded-out").value = decoded; } catch { document.getElementById("decoded-out").value = "❌ Невалиден ключ"; } } // ─── Init ──────────────────────────────────────────────────── document.addEventListener("DOMContentLoaded", () => { toggleEnhance(); // Keyboard shortcut: Ctrl+Enter to synthesize document.getElementById("synth-text").addEventListener("keydown", (e) => { if ((e.ctrlKey || e.metaKey) && e.key === "Enter") synthesize(); }); // If there's already a key on load (browser autofill), fetch voices if (apiKey()) loadVoices(); });