ok the download button next to this is a pdf to tts apk i made there are no ads on it and nothing funny it runs like shit and i will try again at some point
https://drive.google.com/file/d/1n1n2hEDYunPjCebKhFiKcbvAvVslOym8/view?usp=drive_link
https://drive.google.com/file/d/10CUocYWfa_0MYlVrnIq1Upov6efcqs6_/view?usp=drive_link
https://drive.google.com/file/d/1MECxQD1q-jM4Whr9Bw4WqABymXNJO_4c/view?usp=drive_link
daoism
-----------------------------------------------------------------------------------------------------------------------------------------------------
https://www.youtube.com/watch?v=1Ny-1zHw5AI&t=762s
the embed so you can use this tts on your site
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Text/PDF/EPUB to Voice App</title>
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- PDF.js CDN for parsing PDFs -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf_viewer.min.css">
<!-- epub.js CDN for parsing EPUBs -->
<script src="https://unpkg.com/epubjs/dist/epub.min.js"></script>
<style>
body {
font-family: 'Inter', sans-serif;
background-color: #1a1a1a; /* Dark background */
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
color: #ffffff; /* White text for body */
}
.container {
max-width: 800px;
width: 100%;
background-color: #2c2c2c; /* Darker background for container */
border-radius: 16px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4); /* Darker shadow */
overflow: hidden;
border: 2px solid #22c55e; /* Green border */
}
.message-box {
background-color: #3b3b3b; /* Darker blue for messages */
color: #93c5fd; /* Light blue text */
padding: 12px 20px;
border-radius: 8px;
margin-top: 16px;
display: none; /* Hidden by default */
align-items: center;
}
.message-box.error {
background-color: #5f2120; /* Dark red for error background */
color: #fca5a5; /* Light red text */
}
.message-box.success {
background-color: #166534; /* Dark green for success background */
color: #a7f3d0; /* Light green text */
}
.spinner {
border: 4px solid rgba(255, 255, 255, 0.3); /* White spinner border */
border-left-color: #22c55e; /* Green spinner */
border-radius: 50%;
width: 24px;
height: 24px;
animation: spin 1s linear infinite;
margin-right: 10px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Style for highlighted text */
.highlight-word {
background-color: #a7f3d0; /* Light green for highlighting on dark background */
color: #1a1a1a; /* Dark text for highlighted word */
border-radius: 4px;
padding: 0 2px;
transition: background-color 0.2s ease;
}
/* Ensure contenteditable div preserves whitespace and scrolls */
#contentDisplayArea {
white-space: pre-wrap; /* Preserves whitespace and wraps text */
overflow-y: auto; /* Adds scrollbar if content overflows vertically */
}
</style>
</head>
<body class="bg-gray-100 flex items-center justify-center min-h-screen p-4">
<div class="container p-8 space-y-6">
<h1 class="text-3xl font-bold text-center text-white mb-6">Text & PDF/EPUB to Voice</h1>
<!-- Message Box -->
<div id="message-box" class="message-box">
<span id="message-icon"></span>
<span id="message-text"></span>
</div>
<div class="flex flex-col md:flex-row gap-6">
<!-- File Input Section (now handles PDF and EPUB) -->
<div class="flex-1 bg-gray-800 p-6 rounded-xl border border-gray-700 shadow-sm">
<h2 class="text-xl font-semibold text-white mb-4">Upload File</h2>
<input type="file" id="fileInput" accept=".pdf,.epub"
class="w-full text-white file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-green-700 file:text-white hover:file:bg-green-800 cursor-pointer mb-4">
<button id="readFileBtn"
class="w-full bg-green-600 hover:bg-green-700 text-white font-semibold py-3 px-6 rounded-lg shadow-md transition duration-300 transform hover:scale-105">
Read File
</button>
<div id="loadingIndicator" class="hidden mt-4 text-center text-green-400 font-medium flex items-center justify-center">
<div class="spinner mr-2"></div>
<span>Processing file...</span>
</div>
</div>
<!-- Text Input Section -->
<div class="flex-1 bg-gray-800 p-6 rounded-xl border border-gray-700 shadow-sm">
<h2 class="text-xl font-semibold text-white mb-4">Enter Text</h2>
<div id="contentDisplayArea" contenteditable="true"
class="w-full p-3 border border-gray-600 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent transition duration-200 min-h-[150px] overflow-auto text-white">
<!-- Initial placeholder content for the editable div -->
<span class="text-gray-400">Type or paste your text here...</span>
</div>
<button id="readTextBtn"
class="w-full mt-4 bg-green-600 hover:bg-green-700 text-white font-semibold py-3 px-6 rounded-lg shadow-md transition duration-300 transform hover:scale-105">
Read Text
</button>
</div>
</div>
<!-- Pagination Controls -->
<div class="bg-gray-800 p-6 rounded-xl border border-gray-700 shadow-sm mt-6">
<h2 class="text-xl font-semibold text-white mb-4">Page Navigation</h2>
<div class="flex flex-wrap items-center justify-center gap-2 mb-4">
<button id="skipBack50Btn"
class="bg-gray-700 hover:bg-gray-600 text-white font-semibold py-2 px-4 rounded-lg shadow-md transition duration-300 transform hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed text-sm">
Skip -50
</button>
<button id="skipBack10Btn"
class="bg-gray-700 hover:bg-gray-600 text-white font-semibold py-2 px-4 rounded-lg shadow-md transition duration-300 transform hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed text-sm">
Skip -10
</button>
<button id="prevPageBtn"
class="bg-gray-700 hover:bg-gray-600 text-white font-semibold py-2 px-4 rounded-lg shadow-md transition duration-300 transform hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed text-sm">
Page -1
</button>
<span id="pageCounter" class="text-lg font-medium text-white mx-2">Page 0 of 0</span>
<button id="nextPageBtn"
class="bg-gray-700 hover:bg-gray-600 text-white font-semibold py-2 px-4 rounded-lg shadow-md transition duration-300 transform hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed text-sm">
Page +1
</button>
<button id="skipForward10Btn"
class="bg-gray-700 hover:bg-gray-600 text-white font-semibold py-2 px-4 rounded-lg shadow-md transition duration-300 transform hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed text-sm">
Skip +10
</button>
<button id="skipForward50Btn"
class="bg-gray-700 hover:bg-gray-600 text-white font-semibold py-2 px-4 rounded-lg shadow-md transition duration-300 transform hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed text-sm">
Skip +50
</button>
</div>
</div>
<!-- Visual Insight Section (Image Generation) -->
<div class="bg-gray-800 p-6 rounded-xl border border-gray-700 shadow-sm mt-6 text-center">
<h2 class="text-xl font-semibold text-white mb-4">Visual Insight</h2>
<div id="imageDisplayArea" class="flex items-center justify-center min-h-[200px] bg-gray-900 rounded-lg overflow-hidden p-4">
<div id="imageLoadingIndicator" class="hidden text-green-400 flex flex-col items-center">
<div class="spinner mb-2"></div>
<span>Generating image...</span>
</div>
<img id="generatedImage" src="" alt="Generated image based on text" class="max-w-full max-h-[200px] object-contain rounded-lg hidden">
<p id="imagePlaceholder" class="text-gray-500">Image will appear here...</p>
</div>
</div>
<!-- Voice Controls Section -->
<div class="bg-gray-800 p-6 rounded-xl border border-gray-700 shadow-sm mt-6">
<h2 class="text-xl font-semibold text-white mb-4">Voice Controls</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="voiceSelect" class="block text-sm font-medium text-gray-300 mb-1">Select Voice:</label>
<select id="voiceSelect"
class="w-full p-2 border border-gray-600 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent transition duration-200 bg-gray-700 text-white">
<!-- Voices will be populated by JavaScript -->
</select>
</div>
<div>
<label for="rateSlider" class="block text-sm font-medium text-gray-300 mb-1">Speech Rate:</label>
<input type="range" id="rateSlider" min="0.1" max="10" value="1" step="0.1"
class="w-full h-2 bg-gray-600 rounded-lg appearance-none cursor-pointer accent-green-500">
<span id="rateValue" class="text-sm text-gray-400">1x</span>
</div>
<div>
<label for="pitchSlider" class="block text-sm font-medium text-gray-300 mb-1">Speech Pitch:</label>
<input type="range" id="pitchSlider" min="0" max="2" value="1" step="0.1"
class="w-full h-2 bg-gray-600 rounded-lg appearance-none cursor-pointer accent-green-500">
<span id="pitchValue" class="text-sm text-gray-400">1x</span>
</div>
<div>
<label for="volumeSlider" class="block text-sm font-medium text-gray-300 mb-1">Volume:</label>
<input type="range" id="volumeSlider" min="0" max="1" value="1" step="0.1"
class="w-full h-2 bg-gray-600 rounded-lg appearance-none cursor-pointer accent-green-500">
<span id="volumeValue" class="text-sm text-gray-400">1x</span>
</div>
</div>
<button id="stopBtn"
class="w-full mt-6 bg-red-600 hover:bg-red-700 text-white font-semibold py-3 px-6 rounded-lg shadow-md transition duration-300 transform hover:scale-105">
Stop Reading
</button>
</div>
</div>
<script>
// Ensure PDF.js worker is properly configured
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
const contentDisplayArea = document.getElementById('contentDisplayArea');
const fileInput = document.getElementById('fileInput');
const readTextBtn = document.getElementById('readTextBtn');
const readFileBtn = document.getElementById('readFileBtn');
const stopBtn = document.getElementById('stopBtn');
const voiceSelect = document.getElementById('voiceSelect');
const rateSlider = document.getElementById('rateSlider');
const rateValue = document.getElementById('rateValue');
const pitchSlider = document.getElementById('pitchSlider');
const pitchValue = document.getElementById('pitchValue');
const volumeSlider = document.getElementById('volumeSlider');
const volumeValue = document.getElementById('volumeValue');
const loadingIndicator = document.getElementById('loadingIndicator');
const messageBox = document.getElementById('message-box');
const messageIcon = document.getElementById('message-icon');
const messageText = document.getElementById('message-text');
// Pagination elements
const prevPageBtn = document.getElementById('prevPageBtn');
const nextPageBtn = document.getElementById('nextPageBtn');
const pageCounter = document.getElementById('pageCounter');
const skipBack50Btn = document.getElementById('skipBack50Btn');
const skipBack10Btn = document.getElementById('skipBack10Btn');
const skipForward10Btn = document.getElementById('skipForward10Btn');
const skipForward50Btn = document.getElementById('skipForward50Btn');
// Image generation elements
const imageDisplayArea = document.getElementById('imageDisplayArea');
const generatedImage = document.getElementById('generatedImage');
const imageLoadingIndicator = document.getElementById('imageLoadingIndicator');
const imagePlaceholder = document.getElementById('imagePlaceholder');
let voices = [];
let currentUtterance = null; // To keep track of the current speech utterance
let originalText = ""; // Stores the plain text content of the CURRENT PAGE for highlighting
let fullDocumentText = ""; // Stores the entire text from PDF or user input
let pages = []; // Array to hold text divided into pages
let currentPageIndex = 0; // Current page being displayed
let imageGenerationInterval = null; // To store the interval ID for image generation
// Function to display messages
function showMessage(text, type = 'info', icon = '') {
messageBox.className = `message-box ${type} flex`;
messageIcon.innerHTML = icon;
messageText.textContent = text;
// Hide message after a few seconds
setTimeout(() => {
messageBox.classList.add('hidden');
}, 5000);
}
// Function to populate voice dropdown
function populateVoiceList() {
voices = speechSynthesis.getVoices();
voiceSelect.innerHTML = ''; // Clear existing options
const englishVoices = voices.filter(voice => voice.lang.startsWith('en'));
englishVoices.forEach((voice, index) => {
const option = document.createElement('option');
option.textContent = `${voice.name} (${voice.lang})`;
option.value = index; // Use index as value to easily retrieve voice object
if (voice.default) {
option.selected = true; // Select default English voice if available
}
voiceSelect.appendChild(option);
});
// If no English voices are found, still populate with all voices
if (englishVoices.length === 0) {
voices.forEach((voice, index) => {
const option = document.createElement('option');
option.textContent = `${voice.name} (${voice.lang})`;
option.value = index;
if (voice.default) {
option.selected = true;
}
voiceSelect.appendChild(option);
});
showMessage("No English voices found. Displaying all available voices.", "info", "ℹ️");
}
// If voices are loaded, enable controls
if (voices.length > 0) { // Check against total voices, as filtering might result in empty array
readTextBtn.disabled = false;
readFileBtn.disabled = false;
voiceSelect.disabled = false;
} else {
readTextBtn.disabled = true;
readFileBtn.disabled = true;
voiceSelect.disabled = true;
showMessage("Text-to-speech not supported or no voices found in your browser.", "error", "⚠️");
}
}
// Event listener for voiceschanged (when voices are loaded)
if (speechSynthesis.onvoiceschanged !== undefined) {
speechSynthesis.onvoiceschanged = populateVoiceList;
} else {
// Fallback for browsers that don't fire onvoiceschanged immediately
populateVoiceList();
// Try populating again after a short delay, in case they load asynchronously
setTimeout(populateVoiceList, 1000);
}
// Helper to escape HTML characters
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Function to update the content and apply highlighting
function updateHighlight(charIndex, charLength) {
// Ensure originalText refers to the current page's text
if (!originalText) return;
let before = originalText.substring(0, charIndex);
let highlighted = originalText.substring(charIndex, charIndex + charLength);
let after = originalText.substring(charIndex + charLength);
contentDisplayArea.innerHTML = `${escapeHtml(before)}<span class="highlight-word">${escapeHtml(highlighted)}</span>${escapeHtml(after)}`;
// Removed: highlightedSpan.scrollIntoView({ behavior: 'smooth', block: 'center' });
// This stops the automatic scrolling to the highlighted text.
}
// Function to remove highlighting and reset content
function clearHighlight() {
if (originalText) {
contentDisplayArea.textContent = originalText; // Reset to plain text
}
}
// Function to show image loading state
function showImageLoading() {
imageLoadingIndicator.classList.remove('hidden');
imageLoadingIndicator.classList.add('flex'); // Ensure flex for centering spinner
generatedImage.classList.add('hidden');
imagePlaceholder.classList.add('hidden');
}
// Function to hide image loading state
function hideImageLoading() {
imageLoadingIndicator.classList.add('hidden');
}
// Function to show image placeholder
function showImagePlaceholder() {
imagePlaceholder.classList.remove('hidden');
imageDisplayArea.classList.add('flex'); // Ensure flex for centering placeholder
generatedImage.classList.add('hidden');
}
// Function to clear displayed image
function clearImage() {
generatedImage.src = '';
generatedImage.classList.add('hidden');
hideImageLoading();
showImagePlaceholder();
}
// Function to generate image based on text prompt using Gemini API
async function generateImage(textPrompt) {
if (!textPrompt || textPrompt.trim() === '') {
console.warn("No text prompt for image generation.");
hideImageLoading();
showImagePlaceholder();
return;
}
showImageLoading();
console.log("Attempting to generate image with prompt:", textPrompt); // Log the prompt being sent
try {
// Take a reasonable length for the image prompt to ensure relevance and avoid hitting token limits
const shortPrompt = textPrompt.substring(0, Math.min(textPrompt.length, 150)); // Limit to first 150 chars
console.log("Trimmed image prompt:", shortPrompt); // Log the trimmed prompt
const payload = { instances: { prompt: shortPrompt }, parameters: { "sampleCount": 1} };
const apiKey = ""; // Canvas will provide this API key automatically
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/imagen-3.0-generate-002:predict?key=${apiKey}`;
const response = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await response.json();
console.log("Image API response:", result); // Log the full API response
if (response.ok && result.predictions && result.predictions.length > 0 && result.predictions[0].bytesBase64Encoded) {
const imageUrl = `data:image/png;base64,${result.predictions[0].bytesBase64Encoded}`;
generatedImage.src = imageUrl;
generatedImage.classList.remove('hidden');
hideImageLoading();
imagePlaceholder.classList.add('hidden'); // Hide placeholder if image is generated
showMessage("Image generated successfully!", "success", "🖼️✅");
} else {
console.error("Image generation failed: No image data in response or API error.", result);
showMessage("Image generation failed. " + (result.error ? result.error.message : "No image data or API error."), "error", "🖼️❌");
hideImageLoading();
showImagePlaceholder();
}
} catch (error) {
console.error('Error generating image:', error);
showMessage("Error generating image. Check browser console for details (e.g., network issues).", "error", "🖼️❌");
hideImageLoading();
showImagePlaceholder();
}
}
// Function to speak text of the current page
function speakCurrentPage() {
if (speechSynthesis.speaking) {
speechSynthesis.cancel(); // Stop current speech if any
}
// Clear any existing image generation interval
if (imageGenerationInterval) {
clearInterval(imageGenerationInterval);
}
clearImage(); // Clear any existing image
if (pages.length === 0 || currentPageIndex >= pages.length) {
showMessage("No text available to read.", "error", "❗");
clearHighlight();
return;
}
const textToSpeak = pages[currentPageIndex];
if (!textToSpeak.trim()) {
showMessage("Current page is empty. Moving to next available page.", "info", "➡️");
// Attempt to move to next non-empty page
if (currentPageIndex < pages.length - 1) {
currentPageIndex++;
updatePageDisplay();
speakCurrentPage();
} else {
showMessage("End of document reached.", "info", "✅");
clearHighlight();
}
return;
}
originalText = textToSpeak; // Store the current page's text for highlighting
contentDisplayArea.textContent = originalText; // Display plain text initially before speaking
currentUtterance = new SpeechSynthesisUtterance(textToSpeak);
const selectedVoiceIndex = voiceSelect.value;
if (selectedVoiceIndex !== null && voices[selectedVoiceIndex]) {
currentUtterance.voice = voices[selectedVoiceIndex];
}
currentUtterance.rate = parseFloat(rateSlider.value);
currentUtterance.pitch = parseFloat(pitchSlider.value);
currentUtterance.volume = parseFloat(volumeSlider.value);
currentUtterance.onboundary = (event) => {
if (event.name === 'word' || event.name === 'sentence') {
updateHighlight(event.charIndex, event.charLength);
}
};
currentUtterance.onend = () => {
console.log('Speech finished for current page');
currentUtterance = null;
clearHighlight(); // Clear highlighting when speech ends
if (imageGenerationInterval) {
clearInterval(imageGenerationInterval);
}
clearImage(); // Clear image when speech ends
// Optionally move to the next page automatically after reading
// if (currentPageIndex < pages.length - 1) {
// currentPageIndex++;
// updatePageDisplay();
// speakCurrentPage();
// } else {
// showMessage("End of document reached.", "info", "✅");
// }
};
currentUtterance.onerror = (event) => {
console.error('Speech synthesis error:', event.error);
showMessage(`Speech error: ${event.error}`, "error", "❌");
currentUtterance = null;
clearHighlight(); // Clear highlighting on error
if (imageGenerationInterval) {
clearInterval(imageGenerationInterval);
}
clearImage(); // Clear image on error
};
speechSynthesis.speak(currentUtterance);
// Start image generation and interval only if text is available
if (textToSpeak.trim().length > 0) {
const imagePrompt = textToSpeak.substring(0, Math.min(textToSpeak.length, 250)); // Use first 250 chars
generateImage(imagePrompt);
imageGenerationInterval = setInterval(() => generateImage(imagePrompt), 24000); // Changed interval to 24 seconds
}
}
// Function to update the displayed page and pagination controls
function updatePageDisplay() {
clearHighlight(); // Always clear highlight when changing pages
if (imageGenerationInterval) {
clearInterval(imageGenerationInterval);
}
clearImage(); // Clear image when page changes
if (pages.length > 0) {
contentDisplayArea.textContent = pages[currentPageIndex];
pageCounter.textContent = `Page ${currentPageIndex + 1} of ${pages.length}`;
prevPageBtn.disabled = (currentPageIndex === 0);
nextPageBtn.disabled = (currentPageIndex === pages.length - 1);
skipBack10Btn.disabled = (currentPageIndex < 10);
skipBack50Btn.disabled = (currentPageIndex < 50);
skipForward10Btn.disabled = (currentPageIndex + 10 >= pages.length);
skipForward50Btn.disabled = (currentPageIndex + 50 >= pages.length);
originalText = pages[currentPageIndex]; // Set originalText for the new page
setPlaceholder(); // Re-evaluate placeholder for the new content
} else {
contentDisplayArea.textContent = '';
pageCounter.textContent = 'Page 0 of 0';
prevPageBtn.disabled = true;
nextPageBtn.disabled = true;
skipBack10Btn.disabled = true;
skipBack50Btn.disabled = true;
skipForward10Btn.disabled = true;
skipForward50Btn.disabled = true;
originalText = "";
setPlaceholder(); // Ensure placeholder is shown if no content
}
}
// Function to navigate pages
function navigatePages(offset) {
if (speechSynthesis.speaking) speechSynthesis.cancel(); // Stop current speech
currentPageIndex = Math.max(0, Math.min(pages.length - 1, currentPageIndex + offset));
updatePageDisplay();
speakCurrentPage();
}
// Event listener for Read Text button
readTextBtn.addEventListener('click', () => {
const textFromInput = contentDisplayArea.textContent.trim();
if (textFromInput) {
fullDocumentText = textFromInput;
// Split text into pages. Simple split by multiple newlines to simulate paragraphs/sections
pages = fullDocumentText.split(/\n\s*\n/).filter(page => page.trim() !== '');
if (pages.length === 0) {
pages.push(fullDocumentText); // If no obvious pages, treat entire text as one page
}
currentPageIndex = 0;
updatePageDisplay();
speakCurrentPage();
} else {
showMessage("Please type or paste text to read.", "error", "❗");
}
});
// Function to extract text from PDF
async function extractTextFromPdf(file) {
loadingIndicator.classList.remove('hidden');
readFileBtn.disabled = true; // Use readFileBtn
readTextBtn.disabled = true;
try {
const arrayBuffer = await file.arrayBuffer();
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
let textChunks = [];
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const textContent = await page.getTextContent();
const pageText = textContent.items.map(item => item.str).join(' ');
textChunks.push(pageText);
}
return textChunks.join('\n\n'); // Join chunks from each PDF page with a double newline
} catch (error) {
console.error('Error extracting text from PDF:', error);
showMessage("Failed to process PDF. Make sure it's a valid PDF file.", "error", "❗");
return null;
} finally {
loadingIndicator.classList.add('hidden');
readFileBtn.disabled = false; // Use readFileBtn
readTextBtn.disabled = false;
}
}
// Function to extract text from EPUB
async function extractTextFromEpub(file) {
loadingIndicator.classList.remove('hidden');
readFileBtn.disabled = true; // Use readFileBtn
readTextBtn.disabled = true;
try {
const url = URL.createObjectURL(file);
const book = ePub(url);
await book.ready; // Wait for the book to be parsed
let fullEpubText = '';
const spine = book.spine.items;
for (const item of spine) {
const contents = await item.load();
if (contents && contents.textContent) {
fullEpubText += contents.textContent + '\n\n'; // Add content and spacing
}
}
URL.revokeObjectURL(url); // Clean up the URL object
return fullEpubText;
} catch (error) {
console.error('Error extracting text from EPUB:', error);
showMessage("Failed to process EPUB. Make sure it's a valid EPUB file.", "error", "❗");
return null;
} finally {
loadingIndicator.classList.add('hidden');
readFileBtn.disabled = false; // Use readFileBtn
readTextBtn.disabled = false;
}
}
// Event listener for Read File button (now handles PDF and EPUB)
readFileBtn.addEventListener('click', async () => {
const file = fileInput.files[0];
if (!file) {
showMessage("Please select a PDF or EPUB file first.", "error", "❗");
return;
}
let text = null;
if (file.type === 'application/pdf') {
text = await extractTextFromPdf(file);
} else if (file.type === 'application/epub+zip') {
text = await extractTextFromEpub(file);
} else {
showMessage("Unsupported file type. Please upload a PDF or EPUB.", "error", "❗");
return;
}
if (text) {
fullDocumentText = text;
pages = fullDocumentText.split(/\n\s*\n/).filter(page => page.trim() !== '');
if (pages.length === 0) { // If no obvious pages, treat entire text as one page
pages.push(fullDocumentText);
}
currentPageIndex = 0;
updatePageDisplay();
speakCurrentPage();
}
});
// Event listeners for pagination buttons
prevPageBtn.addEventListener('click', () => navigatePages(-1));
nextPageBtn.addEventListener('click', () => navigatePages(1));
skipBack10Btn.addEventListener('click', () => navigatePages(-10));
skipBack50Btn.addEventListener('click', () => navigatePages(-50));
skipForward10Btn.addEventListener('click', () => navigatePages(10));
skipForward50Btn.addEventListener('click', () => navigatePages(50));
// Event listener for Stop button
stopBtn.addEventListener('click', () => {
if (speechSynthesis.speaking) {
speechSynthesis.cancel();
currentUtterance = null;
clearHighlight(); // Clear highlighting when stopped
if (imageGenerationInterval) {
clearInterval(imageGenerationInterval);
}
clearImage(); // Clear image when stopped
showMessage("Speech stopped.", "info", "⏹️");
} else {
showMessage("No speech is currently active.", "info", "💬");
}
});
// Update slider values
rateSlider.addEventListener('input', () => {
rateValue.textContent = `${rateSlider.value}x`;
if (currentUtterance) {
currentUtterance.rate = parseFloat(rateSlider.value);
}
});
pitchSlider.addEventListener('input', () => {
pitchValue.textContent = `${pitchSlider.value}x`;
if (currentUtterance) {
currentUtterance.pitch = parseFloat(pitchSlider.value);
}
});
volumeSlider.addEventListener('input', () => {
volumeValue.textContent = `${volumeSlider.value}x`;
if (currentUtterance) {
currentUtterance.volume = parseFloat(volumeSlider.value);
}
});
// --- Placeholder handling for contenteditable div ---
const placeholderText = 'Type or paste your text here...';
function setPlaceholder() {
// Remove existing placeholder if any
const existingPlaceholder = contentDisplayArea.querySelector('span.text-gray-400');
if (existingPlaceholder) {
contentDisplayArea.removeChild(existingPlaceholder);
}
// If the content area is empty after setting text, show placeholder
if (contentDisplayArea.textContent.trim() === '') {
const placeholderSpan = document.createElement('span');
placeholderSpan.className = 'text-gray-400'; /* Darker gray for placeholder on dark background */
placeholderSpan.textContent = placeholderText;
contentDisplayArea.appendChild(placeholderSpan);
}
}
contentDisplayArea.addEventListener('focus', () => {
const placeholderSpan = contentDisplayArea.querySelector('span.text-gray-400');
if (placeholderSpan && placeholderSpan.parentNode === contentDisplayArea) {
contentDisplayArea.removeChild(placeholderSpan);
}
});
contentDisplayArea.addEventListener('blur', setPlaceholder);
contentDisplayArea.addEventListener('input', () => {
// If user types, remove placeholder immediately if it was still there
const placeholderSpan = contentDisplayArea.querySelector('span.text-gray-400');
if (placeholderSpan && contentDisplayArea.textContent.trim() !== '') {
contentDisplayArea.removeChild(placeholderSpan);
}
});
// Initial setup
populateVoiceList();
updatePageDisplay(); // Initialize page counter and button states
</script>
</body>
</html>