.pdf-speech-reader{border:1px solid #ddd;padding:16px;border-radius:10px;background:#fff;max-width:760px;margin:20px 0}.pdf-speech-reader h3{margin-top:0}.psr-input-row{display:flex;flex-direction:column;gap:8px;margin-bottom:12px}.psr-pdf-url{width:100%;padding:10px}.psr-controls{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px}.psr-controls button,.psr-load-btn{padding:10px 14px;cursor:pointer}.psr-settings{margin-bottom:12px}.psr-status,.psr-progress{margin-bottom:10px;font-size:14px}.psr-text-preview{width:100%;margin-top:8px}.psr-upload-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}(()=>{const PDFJS_WORKER="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js";function splitTextIntoChunks(text,maxLength=900){const cleaned=text.replace(/\s+/g," ").trim();if (!cleaned) return [];const sentences=cleaned.match(/[^.!?]+[.!?]+|[^.!?]+$/g) || [cleaned];const chunks=[];let current="";for (const sentence of sentences){const next=current ? `${current}${sentence}` :sentence;if (next.length <=maxLength){current=next}else{if (current) chunks.push(current.trim());if (sentence.length>maxLength){const words=sentence.split(" ");let temp="";for (const word of words){const candidate=temp ? `${temp}${word}` :word;if (candidate.length <=maxLength){temp=candidate}else{if (temp) chunks.push(temp.trim());temp=word}}current=temp || ""}else{current=sentence}}}if (current) chunks.push(current.trim());return chunks}function getAvailableVoices(){return window.speechSynthesis.getVoices() || []}async function loadPdfText(url,statusEl,progressEl){if (!url){throw new Error("Please provide a PDF URL.")}if (!window.pdfjsLib){throw new Error("PDF.js failed to load.")}pdfjsLib.GlobalWorkerOptions.workerSrc=PDFJS_WORKER;statusEl.textContent="Loading PDF...";progressEl.textContent="";const loadingTask=pdfjsLib.getDocument(url);const pdf=await loadingTask.promise;let combinedText="";let extractedPages=0;for (let pageNum=1;pageNum <=pdf.numPages;pageNum++){statusEl.textContent=`Processing page ${pageNum}of ${pdf.numPages}...`;progressEl.textContent=`Extracting text from page ${pageNum}/${pdf.numPages}`;const page=await pdf.getPage(pageNum);const textContent=await page.getTextContent();const pageText=textContent.items.map((item)=>item.str).join(" ").trim();if (pageText){combinedText+=pageText+"\n\n";extractedPages+=1}}const finalText=combinedText.trim();if (!finalText){throw new Error("No readable text found. This PDF may need OCR.")}statusEl.textContent="PDF loaded successfully.";progressEl.textContent=`Loaded ${pdf.numPages}page(s). Text found on ${extractedPages}page(s).`;return finalText}function stopSpeech(state,statusEl,progressEl){window.speechSynthesis.cancel();state.activeUtterance=null;state.isPaused=false;state.isSpeaking=false;state.currentChunkIndex=0;if (statusEl) statusEl.textContent="Stopped.";if (progressEl && state.chunks.length){progressEl.textContent=`Ready to play ${state.chunks.length}chunk(s).`}}function populateVoiceSelect(voiceSelect){const voices=getAvailableVoices();const currentValue=voiceSelect.value;voiceSelect.innerHTML="";const defaultOption=document.createElement("option");defaultOption.value="";defaultOption.textContent="Default voice";voiceSelect.appendChild(defaultOption);voices.forEach((voice,index)=>{const option=document.createElement("option");option.value=String(index);option.textContent=`${voice.name}(${voice.lang})`;voiceSelect.appendChild(option)});if ([...voiceSelect.options].some((opt)=>opt.value===currentValue)){voiceSelect.value=currentValue}}function speakChunks(state,statusEl,progressEl,rate,voiceSelect){if (!state.chunks.length){statusEl.textContent="No text available to read.";return}if (state.currentChunkIndex>=state.chunks.length){state.currentChunkIndex=0}state.isSpeaking=true;state.isPaused=false;const speakNext=()=>{if (!state.isSpeaking || state.isPaused) return;if (state.currentChunkIndex>=state.chunks.length){statusEl.textContent="Finished reading.";progressEl.textContent=`Completed ${state.chunks.length}chunk(s).`;state.activeUtterance=null;state.isSpeaking=false;state.isPaused=false;state.currentChunkIndex=0;return}const chunk=state.chunks[state.currentChunkIndex];const utterance=new SpeechSynthesisUtterance(chunk);utterance.rate=rate;const voices=getAvailableVoices();const selectedIndex=voiceSelect.value;if (selectedIndex !=="" && voices[selectedIndex]){utterance.voice=voices[selectedIndex]}utterance.onstart=()=>{statusEl.textContent="Reading aloud...";progressEl.textContent=`Speaking chunk ${state.currentChunkIndex+1}of ${state.chunks.length}`};utterance.onend=()=>{state.currentChunkIndex+=1;speakNext()};utterance.onerror=(event)=>{statusEl.textContent=`Speech error:${event.error || "Unknown error"}`;state.activeUtterance=null;state.isSpeaking=false;state.isPaused=false};state.activeUtterance=utterance;window.speechSynthesis.speak(utterance)};window.speechSynthesis.cancel();speakNext()}async function uploadPdfFile(file,uploadStatusEl,urlInput){if (!file) return;if (file.type !=="application/pdf"){uploadStatusEl.textContent="Only PDF files are allowed.";return}const formData=new FormData();formData.append("action","psr_upload_pdf");formData.append("nonce",PdfSpeechReaderConfig.nonce);formData.append("pdf_file",file);uploadStatusEl.textContent="Uploading PDF...";try{const response=await fetch(PdfSpeechReaderConfig.ajaxUrl,{method:"POST",body:formData,credentials:"same-origin",});const result=await response.json();if (!result.success){throw new Error(result?.data?.message || "Upload failed.")}urlInput.value=result.data.url;uploadStatusEl.textContent=`Uploaded successfully:${result.data.file}`}catch (error){console.error(error);uploadStatusEl.textContent=error.message || "Upload failed."}}function initReader(container){const state={fullText:"",chunks:[],activeUtterance:null,isPaused:false,isSpeaking:false,currentChunkIndex:0,};const initialPdfUrl=container.dataset.pdfUrl || "";const urlInput=container.querySelector(".psr-pdf-url");const fileInput=container.querySelector(".psr-file-input");const selectFileBtn=container.querySelector(".psr-select-file-btn");const dropZone=container.querySelector(".psr-drop-zone");const uploadStatusEl=container.querySelector(".psr-upload-status");const loadBtn=container.querySelector(".psr-load-btn");const playBtn=container.querySelector(".psr-play-btn");const pauseBtn=container.querySelector(".psr-pause-btn");const resumeBtn=container.querySelector(".psr-resume-btn");const stopBtn=container.querySelector(".psr-stop-btn");const rateInput=container.querySelector(".psr-rate");const rateValue=container.querySelector(".psr-rate-value");const statusEl=container.querySelector(".psr-status");const progressEl=container.querySelector(".psr-progress");const previewEl=container.querySelector(".psr-text-preview");let voiceSelect=container.querySelector(".psr-voice");if (!voiceSelect){const settingsEl=container.querySelector(".psr-settings");const voiceWrap=document.createElement("label");voiceWrap.style.display="block";voiceWrap.style.marginTop="10px";voiceWrap.innerHTML=` Voice <select class="psr-voice" style="margin-left:8px; max-width:100%;"></select>`;settingsEl.appendChild(voiceWrap);voiceSelect=settingsEl.querySelector(".psr-voice")}populateVoiceSelect(voiceSelect);if (typeof speechSynthesis !=="undefined"){speechSynthesis.onvoiceschanged=()=>populateVoiceSelect(voiceSelect)}if (initialPdfUrl && !urlInput.value){urlInput.value=initialPdfUrl}rateInput.addEventListener("input",()=>{rateValue.textContent=`${Number(rateInput.value).toFixed(1)}x`});async function handleLoad(){try{stopSpeech(state);statusEl.textContent="Preparing...";progressEl.textContent="";const pdfUrl=urlInput.value.trim();state.fullText="";state.chunks=[];previewEl.value="";playBtn.disabled=true;pauseBtn.disabled=true;resumeBtn.disabled=true;stopBtn.disabled=true;state.fullText=await loadPdfText(pdfUrl,statusEl,progressEl);state.chunks=splitTextIntoChunks(state.fullText,900);previewEl.value=state.fullText;if (!state.chunks.length){throw new Error("Text was extracted, but no readable chunks were created.")}playBtn.disabled=false;pauseBtn.disabled=false;resumeBtn.disabled=false;stopBtn.disabled=false;statusEl.textContent="Ready to play.";progressEl.textContent=`Prepared ${state.chunks.length}chunk(s).`}catch (error){console.error(error);statusEl.textContent=error.message || "Could not load PDF.";progressEl.textContent=""}}selectFileBtn.addEventListener("click",()=>{fileInput.click()});fileInput.addEventListener("change",async (event)=>{const file=event.target.files[0];await uploadPdfFile(file,uploadStatusEl,urlInput)});dropZone.addEventListener("dragover",(event)=>{event.preventDefault();dropZone.classList.add("is-dragover")});dropZone.addEventListener("dragleave",()=>{dropZone.classList.remove("is-dragover")});dropZone.addEventListener("drop",async (event)=>{event.preventDefault();dropZone.classList.remove("is-dragover");const file=event.dataTransfer.files[0];await uploadPdfFile(file,uploadStatusEl,urlInput)});loadBtn.addEventListener("click",handleLoad);playBtn.addEventListener("click",()=>{state.currentChunkIndex=0;speakChunks(state,statusEl,progressEl,parseFloat(rateInput.value),voiceSelect)});pauseBtn.addEventListener("click",()=>{if (window.speechSynthesis.speaking && !window.speechSynthesis.paused){window.speechSynthesis.pause();state.isPaused=true;statusEl.textContent="Paused."}});resumeBtn.addEventListener("click",()=>{if (window.speechSynthesis.paused && state.isPaused){window.speechSynthesis.resume();state.isPaused=false;statusEl.textContent="Resumed."}});stopBtn.addEventListener("click",()=>{stopSpeech(state,statusEl,progressEl)});if (urlInput.value.trim()){handleLoad()}}document.addEventListener("DOMContentLoaded",()=>{document.querySelectorAll(".pdf-speech-reader").forEach(initReader)})})();