VOX33 / static /app.js
ssasio's picture
Upload app.js
80ecb01 verified
/* ── 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
? `<span class="spin">⟳</span> ${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) => `<option value="${v.id}">${v.name}</option>`).join("")
: `<option value="">β€” ΠΈΠ·Π±Π΅Ρ€ΠΈ β€”</option>` +
voices.map((v) => `<option value="${v.id}">${v.name} (${v.id})</option>`).join("");
}
function clearVoiceDropdowns() {
["voice-select", "dl-voice", "del-voice"].forEach((id) => {
document.getElementById(id).innerHTML = `<option value="">β€” Π½Π΅Π²Π°Π»ΠΈΠ΄Π΅Π½ ΠΊΠ»ΡŽΡ‡ β€”</option>`;
});
["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();
});