Spaces:
Sleeping
Sleeping
| (function () { | |
| const state = { | |
| defaults: null, | |
| providers: [], | |
| voices: [], | |
| voicesLoaded: false, | |
| activeVoiceFilter: "", | |
| payload: null, | |
| queueIndex: 0, | |
| lastRequest: null, | |
| voiceLoadTimer: null, | |
| voicesProvider: null, | |
| }; | |
| const elements = { | |
| form: document.getElementById("tts-form"), | |
| providerSelect: document.getElementById("provider-select"), | |
| langSelect: document.getElementById("lang-select"), | |
| slowInput: document.getElementById("slow-input"), | |
| textInput: document.getElementById("text-input"), | |
| edgeControls: document.getElementById("edge-controls"), | |
| voiceFilterInput: document.getElementById("voice-filter-input"), | |
| voiceSelect: document.getElementById("voice-select"), | |
| loadVoicesButton: document.getElementById("load-voices-button"), | |
| rateInput: document.getElementById("rate-input"), | |
| volumeInput: document.getElementById("volume-input"), | |
| pitchInput: document.getElementById("pitch-input"), | |
| generateButton: document.getElementById("generate-button"), | |
| sampleButton: document.getElementById("sample-button"), | |
| statusPill: document.getElementById("status-pill"), | |
| summaryBadge: document.getElementById("summary-badge"), | |
| messageBox: document.getElementById("message-box"), | |
| player: document.getElementById("player"), | |
| openLink: document.getElementById("open-link"), | |
| warningsBox: document.getElementById("warnings-box"), | |
| directUrl: document.getElementById("direct-url"), | |
| proxyUrl: document.getElementById("proxy-url"), | |
| jsonOutput: document.getElementById("json-output"), | |
| directUrlLabel: document.getElementById("direct-url-label"), | |
| proxyUrlLabel: document.getElementById("proxy-url-label"), | |
| jsonOutputLabel: document.getElementById("json-output-label"), | |
| baseHtmlSnippet: document.getElementById("base-html-snippet"), | |
| exampleGetUrl: document.getElementById("example-get-url"), | |
| exampleAudioUrl: document.getElementById("example-audio-url"), | |
| exampleVoicesUrl: document.getElementById("example-voices-url"), | |
| audioTagSnippet: document.getElementById("audio-tag-snippet"), | |
| fetchGetSnippet: document.getElementById("fetch-get-snippet"), | |
| fetchPostSnippet: document.getElementById("fetch-post-snippet"), | |
| translateResponseSnippet: document.getElementById("translate-response-snippet"), | |
| edgeResponseSnippet: document.getElementById("edge-response-snippet"), | |
| processSummary: document.getElementById("process-summary"), | |
| }; | |
| bindEvents(); | |
| bootstrap(); | |
| function bindEvents() { | |
| elements.form.addEventListener("submit", handleSubmit); | |
| elements.providerSelect.addEventListener("change", syncProviderMode); | |
| elements.loadVoicesButton.addEventListener("click", loadVoices); | |
| elements.textInput.addEventListener("input", updateExamples); | |
| elements.langSelect.addEventListener("change", updateExamples); | |
| elements.providerSelect.addEventListener("change", updateExamples); | |
| elements.slowInput.addEventListener("change", updateExamples); | |
| elements.voiceSelect.addEventListener("change", updateExamples); | |
| elements.rateInput.addEventListener("input", updateExamples); | |
| elements.volumeInput.addEventListener("input", updateExamples); | |
| elements.pitchInput.addEventListener("input", updateExamples); | |
| elements.voiceFilterInput.addEventListener("input", function () { | |
| const nextFilter = elements.voiceFilterInput.value.trim(); | |
| if (nextFilter !== state.activeVoiceFilter) { | |
| state.voicesLoaded = false; | |
| state.voices = []; | |
| renderVoices(); | |
| scheduleVoiceLoad(); | |
| } | |
| }); | |
| elements.sampleButton.addEventListener("click", function () { | |
| if (state.defaults) { | |
| elements.textInput.value = state.defaults.text || ""; | |
| } | |
| elements.textInput.focus(); | |
| updateExamples(); | |
| }); | |
| elements.player.addEventListener("ended", handlePlayerEnded); | |
| document.querySelectorAll("[data-copy-target]").forEach(function (button) { | |
| button.addEventListener("click", function () { | |
| copyField(button.getAttribute("data-copy-target")); | |
| }); | |
| }); | |
| } | |
| async function bootstrap() { | |
| setStatus("Loading"); | |
| setMessage("Loading configuration..."); | |
| try { | |
| const bootstrapData = await fetchJson("/api/bootstrap"); | |
| state.defaults = bootstrapData.defaults || {}; | |
| state.providers = bootstrapData.providers || []; | |
| renderLanguages(); | |
| applyDefaults(); | |
| setStatus("Ready"); | |
| setMessage("Enter text and click Generate audio."); | |
| } catch (error) { | |
| setStatus("Error"); | |
| setMessage(error.message || "Could not load configuration."); | |
| } | |
| } | |
| function renderProviders() { | |
| elements.providerSelect.innerHTML = ""; | |
| state.providers.forEach(function (provider) { | |
| const option = document.createElement("option"); | |
| option.value = provider.id; | |
| option.textContent = provider.displayName; | |
| elements.providerSelect.appendChild(option); | |
| }); | |
| } | |
| function renderLanguages() { | |
| elements.langSelect.innerHTML = ""; | |
| (state.defaults.translateLanguages || []).forEach(function (language) { | |
| const option = document.createElement("option"); | |
| option.value = language.code; | |
| option.textContent = language.label + " (" + language.code + ")"; | |
| elements.langSelect.appendChild(option); | |
| }); | |
| } | |
| function applyDefaults() { | |
| renderProviders(); | |
| elements.providerSelect.value = state.defaults.provider || "edge"; | |
| elements.langSelect.value = state.defaults.lang || "en"; | |
| elements.textInput.value = state.defaults.text || ""; | |
| elements.voiceFilterInput.value = ""; | |
| elements.rateInput.value = state.defaults.rate || "+0%"; | |
| elements.volumeInput.value = state.defaults.volume || "+0%"; | |
| elements.pitchInput.value = state.defaults.pitch || "+0Hz"; | |
| renderVoices(); | |
| syncProviderMode(); | |
| updateExamples(); | |
| } | |
| async function loadVoices() { | |
| const provider = elements.providerSelect.value || "edge"; | |
| if ( | |
| state.voicesLoaded && | |
| state.voicesProvider === provider && | |
| state.activeVoiceFilter === elements.voiceFilterInput.value.trim() | |
| ) { | |
| renderVoices(); | |
| return; | |
| } | |
| setMessage("Loading available voices..."); | |
| elements.loadVoicesButton.disabled = true; | |
| try { | |
| const filter = elements.voiceFilterInput.value.trim(); | |
| const voicesUrl = filter | |
| ? "/api/voices?provider=" + encodeURIComponent(provider) + "&filter=" + encodeURIComponent(filter) | |
| : "/api/voices?provider=" + encodeURIComponent(provider); | |
| const response = await fetchJson(voicesUrl); | |
| state.voices = response.items || []; | |
| state.voicesLoaded = true; | |
| state.voicesProvider = provider; | |
| state.activeVoiceFilter = filter; | |
| renderVoices(); | |
| setMessage( | |
| state.voices.length | |
| ? "Loaded " + state.voices.length + " voices." | |
| : "No voices matched this filter. Try clearing the filter." | |
| ); | |
| } catch (error) { | |
| state.voices = []; | |
| state.voicesLoaded = false; | |
| renderVoices(); | |
| setMessage(error.message || "Could not load voices."); | |
| } finally { | |
| elements.loadVoicesButton.disabled = false; | |
| } | |
| } | |
| function renderVoices() { | |
| elements.voiceSelect.innerHTML = ""; | |
| if (!state.voices.length) { | |
| const option = document.createElement("option"); | |
| option.value = state.defaults ? state.defaults.voice : "en-US-AriaNeural"; | |
| option.textContent = state.voicesLoaded ? "No voices found" : "Loading voices..."; | |
| elements.voiceSelect.appendChild(option); | |
| return; | |
| } | |
| state.voices.forEach(function (voice, index) { | |
| const option = document.createElement("option"); | |
| option.value = voice.name; | |
| option.textContent = voice.name + " (" + voice.locale + ", " + voice.gender + ")"; | |
| const isKokoroDefault = elements.providerSelect.value === "kokoro" && voice.name === "af_heart"; | |
| if (index === 0 || isKokoroDefault || voice.name === (state.defaults && state.defaults.voice)) { | |
| option.selected = true; | |
| } | |
| elements.voiceSelect.appendChild(option); | |
| }); | |
| } | |
| function syncProviderMode() { | |
| const provider = elements.providerSelect.value; | |
| elements.edgeControls.classList.toggle("hidden", provider !== "edge" && provider !== "kokoro" && provider !== "supertonic"); | |
| if (provider === "kokoro" || provider === "supertonic") { | |
| elements.voiceFilterInput.value = ""; | |
| } | |
| updateExamples(); | |
| elements.generateButton.disabled = !provider; | |
| if (state.voicesProvider !== provider) { | |
| state.voicesLoaded = false; | |
| state.voices = []; | |
| state.activeVoiceFilter = ""; | |
| renderVoices(); | |
| } | |
| if ((provider === "edge" || provider === "kokoro" || provider === "supertonic") && !state.voicesLoaded) { | |
| setMessage(provider.toUpperCase() + " provider selected. Loading voices..."); | |
| loadVoices(); | |
| } | |
| } | |
| function scheduleVoiceLoad() { | |
| const provider = elements.providerSelect.value; | |
| if (provider !== "edge" && provider !== "kokoro" && provider !== "supertonic") { | |
| return; | |
| } | |
| if (state.voiceLoadTimer) { | |
| clearTimeout(state.voiceLoadTimer); | |
| } | |
| state.voiceLoadTimer = setTimeout(function () { | |
| loadVoices(); | |
| }, 280); | |
| } | |
| async function handleSubmit(event) { | |
| event.preventDefault(); | |
| const payload = collectPayload(); | |
| const requestUrl = window.location.origin + "/api/synthesize"; | |
| state.lastRequest = { | |
| url: requestUrl, | |
| body: payload, | |
| }; | |
| setBusy(true); | |
| setStatus("Loading"); | |
| setMessage("Generating audio..."); | |
| try { | |
| const response = await fetch("/api/synthesize", { | |
| method: "POST", | |
| headers: { | |
| "Content-Type": "application/json", | |
| }, | |
| body: JSON.stringify(payload), | |
| }); | |
| const body = await response.json(); | |
| if (!response.ok) { | |
| throw new Error(body.detail || "Could not generate audio."); | |
| } | |
| state.payload = body; | |
| state.queueIndex = 0; | |
| renderPayload(); | |
| setStatus("Ready"); | |
| setMessage(body.provider.toUpperCase() + " audio generated successfully."); | |
| autoplayFirst(); | |
| } catch (error) { | |
| setStatus("Error"); | |
| setMessage(error.message || "Could not generate audio."); | |
| } finally { | |
| setBusy(false); | |
| } | |
| } | |
| function collectPayload() { | |
| return { | |
| provider: elements.providerSelect.value, | |
| text: elements.textInput.value.trim(), | |
| lang: elements.langSelect.value, | |
| slow: elements.slowInput.checked, | |
| voice: elements.voiceSelect.value, | |
| rate: elements.rateInput.value.trim() || "+0%", | |
| volume: elements.volumeInput.value.trim() || "+0%", | |
| pitch: elements.pitchInput.value.trim() || "+0Hz", | |
| }; | |
| } | |
| function renderPayload() { | |
| const payload = state.payload || {}; | |
| const requestMeta = state.lastRequest || {}; | |
| const requestUrl = requestMeta.url || (window.location.origin + "/api/synthesize"); | |
| const requestBody = requestMeta.body || collectPayload(); | |
| fillReadonly(elements.directUrl, requestUrl); | |
| fillReadonly(elements.proxyUrl, JSON.stringify(requestBody, null, 2)); | |
| fillReadonly(elements.jsonOutput, JSON.stringify(payload, null, 2)); | |
| elements.directUrlLabel.textContent = "Request URL"; | |
| elements.proxyUrlLabel.textContent = "Request Body"; | |
| elements.jsonOutputLabel.textContent = "POST /api/synthesize response"; | |
| elements.player.src = payload.audioUrl || ""; | |
| elements.summaryBadge.textContent = (payload.provider || "tts").toUpperCase(); | |
| toggleOpenLink(payload.audioUrl || ""); | |
| renderWarnings(["Audio generated via " + (payload.provider || "selected") + " provider."]); | |
| if (elements.player.src) { | |
| elements.player.load(); | |
| } | |
| updateExamples(); | |
| } | |
| function renderWarnings(items) { | |
| elements.warningsBox.innerHTML = ""; | |
| (items || []).forEach(function (item) { | |
| const warning = document.createElement("div"); | |
| warning.className = "warning"; | |
| warning.textContent = item; | |
| elements.warningsBox.appendChild(warning); | |
| }); | |
| } | |
| function autoplayFirst() { | |
| if (!elements.player.src) { | |
| return; | |
| } | |
| const promise = elements.player.play(); | |
| if (promise && typeof promise.catch === "function") { | |
| promise.catch(function () { | |
| setMessage("Audio is ready. If autoplay is blocked, click Play manually."); | |
| }); | |
| } | |
| } | |
| function handlePlayerEnded() { | |
| return; | |
| } | |
| function updateExamples() { | |
| return; | |
| } | |
| async function fetchJson(url) { | |
| const response = await fetch(url); | |
| const body = await response.json(); | |
| if (!response.ok) { | |
| throw new Error(body.detail || "Request failed."); | |
| } | |
| return body; | |
| } | |
| function setBusy(isBusy) { | |
| elements.generateButton.disabled = isBusy; | |
| elements.generateButton.textContent = isBusy ? "Generating..." : "Generate audio"; | |
| } | |
| function setStatus(value) { | |
| elements.statusPill.textContent = value; | |
| } | |
| function setMessage(value) { | |
| elements.messageBox.textContent = value; | |
| } | |
| function fillReadonly(node, value) { | |
| if (!node) { | |
| return; | |
| } | |
| node.value = value || ""; | |
| } | |
| function toggleOpenLink(url) { | |
| if (!url) { | |
| elements.openLink.classList.add("hidden"); | |
| elements.openLink.href = "#"; | |
| return; | |
| } | |
| elements.openLink.classList.remove("hidden"); | |
| elements.openLink.href = url; | |
| } | |
| function copyField(targetId) { | |
| const field = document.getElementById(targetId); | |
| if (!field || !field.value) { | |
| return; | |
| } | |
| if (navigator.clipboard && navigator.clipboard.writeText) { | |
| navigator.clipboard.writeText(field.value).then(function () { | |
| setMessage("Copied: " + targetId); | |
| }); | |
| return; | |
| } | |
| field.focus(); | |
| field.select(); | |
| document.execCommand("copy"); | |
| setMessage("Copied: " + targetId); | |
| } | |
| })(); | |