1/** 2 * GJ Persistent TTS - Frontend Playback 3 * Flow: Cache check â Play â If broken URL â auto-retry with ?force=1 â Generate fresh â Play 4 */ 5(function ($) { 6 "use strict"; 7 8 if (typeof gj_tts_params === "undefined") return; 9 10 var currentAudio = null; 11 var isSpeaking = false; 12 var activeButton = null; 13 var retryAttempted = false; // prevent infinite retry loop 14 15 /* ------------------------------------------------------- 16 * AUDIO CONTROL 17 * ----------------------------------------------------- */ 18 function playAudio(url, button) { 19 console.log("GJ TTS: Attempting to play URL:", url); 20 21 if (!url || typeof url !== "string" || url.length < 10) { 22 console.error("GJ TTS: Invalid audio URL:", url); 23 alert("Invalid audio URL received."); 24 resetButton(button); 25 return; 26 } 27 28 if (currentAudio) { 29 currentAudio.pause(); 30 currentAudio = null; 31 } 32 33 currentAudio = new Audio(); 34 isSpeaking = true; 35 activeButton = button; 36 37 currentAudio.onended = function () { 38 resetButton(button); 39 }; 40 41 currentAudio.onerror = function () { 42 console.error("GJ TTS: Audio not playable:", url); 43 44 // Auto-retry: if cached URL failed, retry with ?force=1 to generate fresh 45 if (!retryAttempted) { 46 console.log( 47 "GJ TTS: Cached URL broken â auto-retrying with force=1...", 48 ); 49 retryAttempted = true; 50 setButtonState(button, "loading"); 51 fetchAudio(button, true); // force=1 52 } else { 53 alert("Audio could not be played. Please try again later."); 54 resetButton(button); 55 } 56 }; 57 58 currentAudio.oncanplaythrough = function () { 59 setButtonState(button, "playing"); 60 currentAudio.play().catch(function (err) { 61 console.error("GJ TTS: Play failed:", err); 62 resetButton(button); 63 }); 64 }; 65 66 currentAudio.src = url; 67 currentAudio.load(); 68 } 69 70 /* ------------------------------------------------------- 71 * BUTTON STATES 72 * ----------------------------------------------------- */ 73 function setButtonState(button, state) { 74 if (!button) return; 75 if (!button.dataset.originalHtml) { 76 button.dataset.originalHtml = button.innerHTML; 77 } 78 79 button.classList.remove("speech-active", "speech-loading"); 80 button.style.background = ""; 81 82 if (state === "loading") { 83 button.classList.add("speech-loading"); 84 button.innerHTML = 85 '<svg width="17" height="17" viewBox="0 0 24 24" fill="none" style="animation:spin 1s linear infinite"><circle cx="12" cy="12" r="10" stroke="white" stroke-width="2" stroke-dasharray="30 70"/></svg> Generating...'; 86 button.style.background = "#555"; 87 } else if (state === "playing") { 88 button.classList.add("speech-active"); 89 button.innerHTML = 90 '<svg width="17" height="17" viewBox="0 0 24 24" fill="white"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg> Stop'; 91 button.style.background = "#e74c3c"; 92 } 93 } 94 95 function resetButton(button) { 96 isSpeaking = false; 97 retryAttempted = false; 98 if (button) { 99 button.classList.remove("speech-active", "speech-loading"); 100 button.innerHTML = button.dataset.originalHtml || "Listen"; 101 button.style.background = ""; 102 } 103 if (currentAudio) { 104 currentAudio.pause(); 105 currentAudio = null; 106 } 107 activeButton = null; 108 } 109 110 function stop() { 111 if (activeButton) resetButton(activeButton); 112 } 113 114 /* ------------------------------------------------------- 115 * API CALL â Fetch audio URL from REST endpoint 116 * force=true skips cache and generates fresh audio 117 * ----------------------------------------------------- */ 118 function fetchAudio(button, force) { 119 var apiUrl = gj_tts_params.rest_url + "get-audio/" + gj_tts_params.post_id; 120 if (force) apiUrl += "?force=1"; 121 122 $.ajax({ 123 url: apiUrl, 124 method: "GET", 125 headers: { "X-WP-Nonce": gj_tts_params.nonce }, 126 success: function (response) { 127 console.log( 128 "GJ TTS: API response" + (force ? " (FORCED)" : "") + ":", 129 JSON.stringify(response), 130 ); 131 if (response && response.status === "success" && response.url) { 132 console.log( 133 "GJ TTS: source=" + 134 (response.source || "?") +
135 " cached=" + 136 response.cached + 137 " url=" + 138 response.url, 139 ); 140 playAudio(response.url, button); 141 } else { 142 var msg = 143 response && response.message 144 ? response.message 145 : "Audio generation failed."; 146 console.error("GJ TTS Error:", msg); 147 alert(msg); 148 resetButton(button); 149 } 150 }, 151 error: function (xhr, status, error) { 152 var serverMsg = ""; 153 try { 154 var json = JSON.parse(xhr.responseText); 155 serverMsg = json.message || ""; 156 } catch (e) { 157 serverMsg = xhr.responseText 158 ? xhr.responseText.substring(0, 200) 159 : error; 160 } 161 console.error("GJ TTS: API Error |", status, "|", serverMsg); 162 alert("Audio load failed: " + serverMsg); 163 resetButton(button); 164 }, 165 }); 166 } 167 168 /* ------------------------------------------------------- 169 * MAIN â Called on button click 170 * ----------------------------------------------------- */ 171 function readSpeechSection(button) { 172 if (isSpeaking) { 173 stop(); 174 return; 175 } 176 177 if (!gj_tts_params.post_id || !gj_tts_params.rest_url) { 178 console.error("GJ TTS: Missing params"); 179 return; 180 } 181 182 retryAttempted = false; 183 setButtonState(button, "loading"); 184 fetchAudio(button, false); // try cache first 185 } 186 187 /* ------------------------------------------------------- 188 * STABILIZER & GLOBAL 189 * ----------------------------------------------------- */ 190 document.addEventListener("visibilitychange", function () { 191 if (document.visibilityState === "hidden") stop(); 192 }); 193 window.addEventListener("pagehide", stop); 194 195 var style = document.createElement("style"); 196 style.textContent = "@keyframes spin { to { transform: rotate(360deg); } }"; 197 document.head.appendChild(style); 198 199 $(document).on("click", "#gj-listen-btn, .gj-listen-btn", function () { 200 readSpeechSection(this); 201 }); 202 203 window.readSpeechSection = readSpeechSection; 204 window.stopSpeech = stop; 205})(jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.