(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); } })();