| |
|
|
| |
| 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; |
| } |
|
|
| |
| 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"); |
| } |
|
|
| |
| 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); |
| } |
|
|
| |
| 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"; |
| } |
|
|
| |
| 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) { |
| |
| 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; |
| } |
| } |
|
|
| |
| 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; |
| } |
| } |
|
|
| |
| 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"); |
| } |
| } |
|
|
| |
| 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; |
| } |
| } |
|
|
| |
| 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 = "β ΠΠ΅Π²Π°Π»ΠΈΠ΄Π΅Π½ ΠΊΠ»ΡΡ"; |
| } |
| } |
|
|
| |
| document.addEventListener("DOMContentLoaded", () => { |
| toggleEnhance(); |
|
|
| |
| document.getElementById("synth-text").addEventListener("keydown", (e) => { |
| if ((e.ctrlKey || e.metaKey) && e.key === "Enter") synthesize(); |
| }); |
|
|
| |
| if (apiKey()) loadVoices(); |
| }); |
|
|