Spaces:
Running
Running
Update index.html
Browse files- index.html +937 -620
index.html
CHANGED
@@ -3,7 +3,7 @@
|
|
3 |
<head>
|
4 |
<meta charset="UTF-8">
|
5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
-
<title>
|
7 |
<script src="https://cdn.tailwindcss.com"></script>
|
8 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
9 |
<style>
|
@@ -200,6 +200,42 @@
|
|
200 |
transition: width 0.3s ease;
|
201 |
}
|
202 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
203 |
/* Library styles */
|
204 |
.library-item {
|
205 |
border: 1px solid var(--border-color);
|
@@ -214,6 +250,11 @@
|
|
214 |
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
215 |
}
|
216 |
|
|
|
|
|
|
|
|
|
|
|
217 |
/* Modal styles */
|
218 |
.modal {
|
219 |
position: fixed;
|
@@ -282,6 +323,58 @@
|
|
282 |
opacity: 0;
|
283 |
}
|
284 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
285 |
/* Accessibility improvements */
|
286 |
.sr-only {
|
287 |
position: absolute;
|
@@ -349,8 +442,8 @@
|
|
349 |
<body class="bg-gray-50 min-h-screen">
|
350 |
<div class="container mx-auto px-4 py-8">
|
351 |
<header class="text-center mb-8 relative">
|
352 |
-
<h1 class="text-4xl font-bold text-blue-600 mb-2">
|
353 |
-
<p class="text-gray-600">Natural HD
|
354 |
|
355 |
<!-- Dark Mode Toggle -->
|
356 |
<button id="darkModeToggle" class="absolute top-0 right-0 p-2 text-gray-600 hover:text-gray-800 transition"
|
@@ -555,7 +648,7 @@
|
|
555 |
|
556 |
<div class="control-buttons flex items-center gap-2">
|
557 |
<button id="synthesizeBtn" class="bg-green-600 hover:bg-green-700 text-white rounded-full w-12 h-12 flex items-center justify-center transition"
|
558 |
-
title="Generate speech" aria-label="Generate speech">
|
559 |
<i class="fas fa-magic" aria-hidden="true"></i>
|
560 |
</button>
|
561 |
<button id="playBtn" class="bg-blue-600 hover:bg-blue-700 text-white rounded-full w-12 h-12 flex items-center justify-center transition"
|
@@ -577,17 +670,16 @@
|
|
577 |
</div>
|
578 |
</div>
|
579 |
|
580 |
-
<!--
|
581 |
-
<div id="
|
582 |
-
<div class="flex justify-between text-sm text-gray-600 mb-
|
583 |
-
<span>
|
584 |
-
<span id="
|
585 |
-
</div>
|
586 |
-
<div class="synthesis-progress">
|
587 |
-
<div id="synthesisProgressBar" class="synthesis-progress-bar" style="width: 0%"></div>
|
588 |
</div>
|
|
|
589 |
</div>
|
590 |
|
|
|
591 |
<div class="progress-container">
|
592 |
<div class="flex justify-between text-sm text-gray-600 mb-1">
|
593 |
<span id="currentTime" aria-live="polite">0:00</span>
|
@@ -624,9 +716,9 @@
|
|
624 |
</div>
|
625 |
</div>
|
626 |
|
627 |
-
<!-- Audio Library Modal -->
|
628 |
<div id="libraryModal" class="modal hidden" role="dialog" aria-modal="true" aria-labelledby="libraryModalTitle">
|
629 |
-
<div class="modal-content max-w-
|
630 |
<div class="flex justify-between items-center mb-6">
|
631 |
<h2 id="libraryModalTitle" class="text-2xl font-semibold">Audio Library</h2>
|
632 |
<button id="closeLibraryModal" class="text-gray-400 hover:text-gray-600" aria-label="Close library">
|
@@ -634,7 +726,8 @@
|
|
634 |
</button>
|
635 |
</div>
|
636 |
|
637 |
-
|
|
|
638 |
<div class="flex items-center gap-2">
|
639 |
<button id="saveToLibraryBtn" class="btn-primary" disabled aria-label="Save current audio to library">
|
640 |
<i class="fas fa-save mr-2" aria-hidden="true"></i> Save Current Audio
|
@@ -647,7 +740,42 @@
|
|
647 |
<div class="text-sm text-gray-500" id="libraryCount" aria-live="polite">0 items</div>
|
648 |
</div>
|
649 |
|
650 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
651 |
<div class="text-center py-8 text-gray-500 col-span-full">
|
652 |
<i class="fas fa-music text-4xl mb-4" aria-hidden="true"></i>
|
653 |
<p>No audio files saved yet</p>
|
@@ -656,6 +784,57 @@
|
|
656 |
</div>
|
657 |
</div>
|
658 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
659 |
<!-- Required libraries for document processing -->
|
660 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.min.js"></script>
|
661 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/mammoth/1.4.21/mammoth.browser.min.js"></script>
|
@@ -681,6 +860,14 @@
|
|
681 |
let audioLibrary = JSON.parse(localStorage.getItem('audioLibrary') || '[]');
|
682 |
let bookmarks = JSON.parse(localStorage.getItem('documentBookmarks') || '[]');
|
683 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
684 |
const MAX_CHUNK_SIZE = 5000;
|
685 |
const PRICING = {
|
686 |
standard: 4.00,
|
@@ -739,9 +926,9 @@
|
|
739 |
const loadingOverlay = document.getElementById('loadingOverlay');
|
740 |
const loadingTitle = document.getElementById('loadingTitle');
|
741 |
const loadingMessage = document.getElementById('loadingMessage');
|
742 |
-
const
|
743 |
-
const
|
744 |
-
const
|
745 |
|
746 |
// Library modal elements
|
747 |
const libraryModal = document.getElementById('libraryModal');
|
@@ -750,10 +937,270 @@
|
|
750 |
const clearLibraryBtn = document.getElementById('clearLibraryBtn');
|
751 |
const libraryContent = document.getElementById('libraryContent');
|
752 |
const libraryCount = document.getElementById('libraryCount');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
753 |
|
754 |
// Dark mode elements
|
755 |
const darkModeToggle = document.getElementById('darkModeToggle');
|
756 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
757 |
// Utility function for file size formatting
|
758 |
function formatFileSize(bytes) {
|
759 |
if (bytes === 0) return '0 Bytes';
|
@@ -833,6 +1280,12 @@
|
|
833 |
// Initialize library
|
834 |
updateLibraryView();
|
835 |
|
|
|
|
|
|
|
|
|
|
|
|
|
836 |
// Event listeners
|
837 |
modelSelect.addEventListener('change', (e) => {
|
838 |
updateCostEstimator();
|
@@ -1295,6 +1748,7 @@
|
|
1295 |
// Store all voices and populate dropdown
|
1296 |
availableVoices = data.voices.sort((a, b) => a.name.localeCompare(b.name));
|
1297 |
populateVoiceDropdown(availableVoices);
|
|
|
1298 |
|
1299 |
// Auto-select standard voice by default
|
1300 |
autoSelectVoiceForModel('standard');
|
@@ -1365,7 +1819,19 @@
|
|
1365 |
});
|
1366 |
}
|
1367 |
|
1368 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1369 |
async function synthesizeSpeech(text, voice, rate = 1, pitch = 0, model = 'standard') {
|
1370 |
if (!apiKey) {
|
1371 |
throw new Error('API key not provided');
|
@@ -1461,7 +1927,6 @@
|
|
1461 |
throw new Error('Invalid response from Google TTS API - no audio content received');
|
1462 |
}
|
1463 |
|
1464 |
-
showToast(`Generated audio chunk (~$${cost.toFixed(4)})`, 'success', 1500);
|
1465 |
return data.audioContent;
|
1466 |
} catch (error) {
|
1467 |
console.error('Synthesis error:', error);
|
@@ -1469,41 +1934,107 @@
|
|
1469 |
}
|
1470 |
}
|
1471 |
|
1472 |
-
//
|
1473 |
-
async function
|
1474 |
-
if (!
|
1475 |
-
|
|
|
1476 |
}
|
1477 |
|
1478 |
-
|
1479 |
-
|
1480 |
-
|
1481 |
-
|
1482 |
-
|
1483 |
-
|
1484 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1485 |
|
1486 |
-
|
1487 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1488 |
updatePlaybackButtons();
|
|
|
|
|
1489 |
|
1490 |
-
|
1491 |
-
|
1492 |
-
|
1493 |
-
|
1494 |
-
|
1495 |
-
|
1496 |
-
|
1497 |
-
|
1498 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1499 |
}
|
1500 |
-
|
|
|
|
|
|
|
|
|
|
|
1501 |
} catch (error) {
|
1502 |
-
console.error('Error
|
1503 |
-
showToast('
|
1504 |
}
|
1505 |
}
|
1506 |
|
|
|
1507 |
function startProgressTracking() {
|
1508 |
function updateProgress() {
|
1509 |
if (!isPlaying || isPaused || !audioSource) return;
|
@@ -1703,25 +2234,21 @@
|
|
1703 |
}
|
1704 |
}
|
1705 |
|
1706 |
-
//
|
1707 |
function readTextFile(file) {
|
1708 |
const reader = new FileReader();
|
1709 |
reader.onload = (e) => {
|
1710 |
-
|
1711 |
-
setDocumentContent(text);
|
1712 |
showToast(`Loaded ${formatFileSize(file.size)} text file`, 'success');
|
1713 |
hideLoadingOverlay();
|
1714 |
-
showStatus(`File loaded: ${file.name}`, 'fa-file-alt');
|
1715 |
};
|
1716 |
reader.onerror = () => {
|
1717 |
showToast('Error reading text file', 'error');
|
1718 |
hideLoadingOverlay();
|
1719 |
-
showStatus('File loading failed', 'fa-exclamation-triangle');
|
1720 |
};
|
1721 |
reader.readAsText(file, 'UTF-8');
|
1722 |
}
|
1723 |
|
1724 |
-
// PDF file reader
|
1725 |
function readPDFFile(file) {
|
1726 |
if (typeof pdfjsLib === 'undefined') {
|
1727 |
showToast('PDF.js not loaded. Please refresh the page and try again.', 'error');
|
@@ -1730,60 +2257,31 @@
|
|
1730 |
}
|
1731 |
|
1732 |
const fileURL = URL.createObjectURL(file);
|
1733 |
-
|
1734 |
-
const loadingTask = pdfjsLib.getDocument({
|
1735 |
-
url: fileURL,
|
1736 |
-
verbosity: 0
|
1737 |
-
});
|
1738 |
|
1739 |
loadingTask.promise.then(pdf => {
|
1740 |
-
let text = '';
|
1741 |
-
const numPages = pdf.numPages;
|
1742 |
const pagePromises = [];
|
1743 |
-
|
1744 |
-
|
1745 |
-
|
1746 |
-
|
1747 |
-
|
1748 |
-
|
1749 |
-
pagesProcessed++;
|
1750 |
-
const progress = (pagesProcessed / numPages) * 100;
|
1751 |
-
loadingMessage.textContent = `Processing page ${pagesProcessed}/${numPages}...`;
|
1752 |
-
|
1753 |
-
return textContent.items.map(item => {
|
1754 |
-
if (item.str) {
|
1755 |
-
return item.str;
|
1756 |
-
}
|
1757 |
-
return '';
|
1758 |
-
}).join(' ');
|
1759 |
-
});
|
1760 |
-
}));
|
1761 |
}
|
1762 |
|
1763 |
Promise.all(pagePromises).then(pagesText => {
|
1764 |
-
|
1765 |
-
setDocumentContent(text);
|
1766 |
-
URL.revokeObjectURL(fileURL);
|
1767 |
-
showToast(`Loaded PDF with ${numPages} pages`, 'success');
|
1768 |
-
hideLoadingOverlay();
|
1769 |
-
showStatus(`PDF loaded: ${numPages} pages`, 'fa-file-pdf');
|
1770 |
-
}).catch(error => {
|
1771 |
-
console.error('Error extracting PDF text:', error);
|
1772 |
-
showToast('Failed to extract text from PDF', 'error');
|
1773 |
URL.revokeObjectURL(fileURL);
|
|
|
1774 |
hideLoadingOverlay();
|
1775 |
-
showStatus('PDF processing failed', 'fa-exclamation-triangle');
|
1776 |
});
|
1777 |
}).catch(error => {
|
1778 |
console.error('Error loading PDF:', error);
|
1779 |
showToast('Failed to load PDF file: ' + error.message, 'error');
|
1780 |
-
URL.revokeObjectURL(fileURL);
|
1781 |
hideLoadingOverlay();
|
1782 |
-
showStatus('PDF loading failed', 'fa-exclamation-triangle');
|
1783 |
});
|
1784 |
}
|
1785 |
|
1786 |
-
// Word document reader
|
1787 |
function readWordDocument(file) {
|
1788 |
if (typeof mammoth === 'undefined') {
|
1789 |
showToast('Mammoth.js not loaded. Please refresh the page and try again.', 'error');
|
@@ -1794,257 +2292,55 @@
|
|
1794 |
const reader = new FileReader();
|
1795 |
reader.onload = async (e) => {
|
1796 |
try {
|
1797 |
-
const
|
1798 |
-
convertImage: mammoth.images.ignoreImages
|
1799 |
-
};
|
1800 |
-
|
1801 |
-
const result = await mammoth.extractRawText({
|
1802 |
-
arrayBuffer: e.target.result
|
1803 |
-
}, options);
|
1804 |
-
|
1805 |
if (result.value && result.value.trim()) {
|
1806 |
setDocumentContent(result.value);
|
1807 |
showToast(`Loaded Word document: ${file.name}`, 'success');
|
1808 |
-
showStatus(`Word document loaded: ${file.name}`, 'fa-file-word');
|
1809 |
} else {
|
1810 |
showToast('No text content found in Word document', 'error');
|
1811 |
-
showStatus('No readable text found', 'fa-exclamation-triangle');
|
1812 |
-
}
|
1813 |
-
|
1814 |
-
if (result.messages && result.messages.length > 0) {
|
1815 |
-
console.warn('Word processing warnings:', result.messages);
|
1816 |
}
|
1817 |
} catch (error) {
|
1818 |
console.error('Error reading Word document:', error);
|
1819 |
-
|
1820 |
-
let errorMessage = 'Error reading Word document.';
|
1821 |
-
if (error.message.includes('Unsupported file format')) {
|
1822 |
-
errorMessage = 'Unsupported Word format. Please use .docx format.';
|
1823 |
-
} else if (error.message.includes('corrupted')) {
|
1824 |
-
errorMessage = 'Document appears to be corrupted. Please try saving it again.';
|
1825 |
-
}
|
1826 |
-
|
1827 |
-
showToast(errorMessage, 'error');
|
1828 |
-
showStatus('Word document processing failed', 'fa-exclamation-triangle');
|
1829 |
} finally {
|
1830 |
hideLoadingOverlay();
|
1831 |
}
|
1832 |
};
|
1833 |
-
|
1834 |
-
reader.onerror = () => {
|
1835 |
-
showToast('Error reading file', 'error');
|
1836 |
-
hideLoadingOverlay();
|
1837 |
-
showStatus('File reading failed', 'fa-exclamation-triangle');
|
1838 |
-
};
|
1839 |
-
|
1840 |
reader.readAsArrayBuffer(file);
|
1841 |
}
|
1842 |
|
1843 |
-
//
|
1844 |
-
function
|
1845 |
const reader = new FileReader();
|
1846 |
reader.onload = (e) => {
|
1847 |
-
let
|
1848 |
-
|
1849 |
-
|
1850 |
-
|
1851 |
-
|
1852 |
-
|
1853 |
-
|
1854 |
-
|
1855 |
-
rtfText = rtfText.replace(/\s+/g, ' ');
|
1856 |
-
rtfText = rtfText.trim();
|
1857 |
-
|
1858 |
-
setDocumentContent(rtfText);
|
1859 |
-
showToast(`Loaded RTF file: ${file.name}`, 'success');
|
1860 |
-
hideLoadingOverlay();
|
1861 |
-
showStatus(`RTF loaded: ${file.name}`, 'fa-file-alt');
|
1862 |
-
};
|
1863 |
-
reader.onerror = () => {
|
1864 |
-
showToast('Error reading RTF file', 'error');
|
1865 |
hideLoadingOverlay();
|
1866 |
-
showStatus('RTF reading failed', 'fa-exclamation-triangle');
|
1867 |
};
|
1868 |
reader.readAsText(file);
|
1869 |
}
|
1870 |
|
1871 |
-
|
1872 |
-
function readMarkdownFile(file) {
|
1873 |
const reader = new FileReader();
|
1874 |
reader.onload = (e) => {
|
1875 |
-
|
1876 |
-
|
1877 |
-
|
1878 |
-
|
1879 |
-
|
1880 |
-
|
1881 |
-
|
1882 |
-
|
1883 |
-
|
1884 |
-
|
1885 |
-
text = text.replace(/^>\s+/gm, ''); // Blockquotes
|
1886 |
-
text = text.replace(/^[\s]*-\s+/gm, '• '); // Unordered lists
|
1887 |
-
text = text.replace(/^[\s]*\d+\.\s+/gm, ''); // Ordered lists
|
1888 |
-
text = text.replace(/---+/g, ''); // Horizontal rules
|
1889 |
-
text = text.replace(/\n{3,}/g, '\n\n'); // Multiple newlines
|
1890 |
-
|
1891 |
setDocumentContent(text);
|
1892 |
-
showToast(`Loaded Markdown file: ${file.name}`, 'success');
|
1893 |
-
hideLoadingOverlay();
|
1894 |
-
showStatus(`Markdown loaded: ${file.name}`, 'fa-file-alt');
|
1895 |
-
};
|
1896 |
-
reader.onerror = () => {
|
1897 |
-
showToast('Error reading Markdown file', 'error');
|
1898 |
hideLoadingOverlay();
|
1899 |
-
showStatus('Markdown reading failed', 'fa-exclamation-triangle');
|
1900 |
-
};
|
1901 |
-
reader.readAsText(file);
|
1902 |
-
}
|
1903 |
-
|
1904 |
-
// JSON file reader
|
1905 |
-
function readJSONFile(file) {
|
1906 |
-
const reader = new FileReader();
|
1907 |
-
reader.onload = (e) => {
|
1908 |
-
try {
|
1909 |
-
const jsonData = JSON.parse(e.target.result);
|
1910 |
-
|
1911 |
-
let text = '';
|
1912 |
-
|
1913 |
-
function jsonToText(obj, prefix = '') {
|
1914 |
-
for (const [key, value] of Object.entries(obj)) {
|
1915 |
-
if (typeof value === 'object' && value !== null) {
|
1916 |
-
text += `${prefix}${key}:\n`;
|
1917 |
-
jsonToText(value, prefix + ' ');
|
1918 |
-
} else {
|
1919 |
-
text += `${prefix}${key}: ${value}\n`;
|
1920 |
-
}
|
1921 |
-
}
|
1922 |
-
}
|
1923 |
-
|
1924 |
-
jsonToText(jsonData);
|
1925 |
-
setDocumentContent(text);
|
1926 |
-
showToast(`Loaded JSON file: ${file.name}`, 'success');
|
1927 |
-
hideLoadingOverlay();
|
1928 |
-
showStatus(`JSON loaded: ${file.name}`, 'fa-file-alt');
|
1929 |
-
} catch (error) {
|
1930 |
-
showToast('Invalid JSON file', 'error');
|
1931 |
-
hideLoadingOverlay();
|
1932 |
-
showStatus('Invalid JSON file', 'fa-exclamation-triangle');
|
1933 |
-
}
|
1934 |
-
};
|
1935 |
-
reader.onerror = () => {
|
1936 |
-
showToast('Error reading JSON file', 'error');
|
1937 |
-
hideLoadingOverlay();
|
1938 |
-
showStatus('JSON reading failed', 'fa-exclamation-triangle');
|
1939 |
-
};
|
1940 |
-
reader.readAsText(file);
|
1941 |
-
}
|
1942 |
-
|
1943 |
-
// HTML file reader
|
1944 |
-
function readHTMLFile(file) {
|
1945 |
-
const reader = new FileReader();
|
1946 |
-
reader.onload = (e) => {
|
1947 |
-
const htmlContent = e.target.result;
|
1948 |
-
|
1949 |
-
const tempDiv = document.createElement('div');
|
1950 |
-
tempDiv.innerHTML = htmlContent;
|
1951 |
-
|
1952 |
-
const scripts = tempDiv.querySelectorAll('script, style');
|
1953 |
-
scripts.forEach(script => script.remove());
|
1954 |
-
|
1955 |
-
let text = tempDiv.textContent || tempDiv.innerText || '';
|
1956 |
-
|
1957 |
-
text = text.replace(/\s+/g, ' ');
|
1958 |
-
text = text.replace(/\n\s*\n/g, '\n\n');
|
1959 |
-
text = text.trim();
|
1960 |
-
|
1961 |
-
setDocumentContent(text);
|
1962 |
-
showToast(`Loaded HTML file: ${file.name}`, 'success');
|
1963 |
-
hideLoadingOverlay();
|
1964 |
-
showStatus(`HTML loaded: ${file.name}`, 'fa-file-alt');
|
1965 |
-
};
|
1966 |
-
reader.onerror = () => {
|
1967 |
-
showToast('Error reading HTML file', 'error');
|
1968 |
-
hideLoadingOverlay();
|
1969 |
-
showStatus('HTML reading failed', 'fa-exclamation-triangle');
|
1970 |
-
};
|
1971 |
-
reader.readAsText(file);
|
1972 |
-
}
|
1973 |
-
|
1974 |
-
// XML file reader
|
1975 |
-
function readXMLFile(file) {
|
1976 |
-
const reader = new FileReader();
|
1977 |
-
reader.onload = (e) => {
|
1978 |
-
try {
|
1979 |
-
const xmlContent = e.target.result;
|
1980 |
-
const parser = new DOMParser();
|
1981 |
-
const xmlDoc = parser.parseFromString(xmlContent, 'text/xml');
|
1982 |
-
|
1983 |
-
let text = '';
|
1984 |
-
|
1985 |
-
function extractText(node) {
|
1986 |
-
if (node.nodeType === Node.TEXT_NODE) {
|
1987 |
-
text += node.textContent + ' ';
|
1988 |
-
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
1989 |
-
for (const child of node.childNodes) {
|
1990 |
-
extractText(child);
|
1991 |
-
}
|
1992 |
-
}
|
1993 |
-
}
|
1994 |
-
|
1995 |
-
extractText(xmlDoc.documentElement);
|
1996 |
-
|
1997 |
-
text = text.replace(/\s+/g, ' ');
|
1998 |
-
text = text.trim();
|
1999 |
-
|
2000 |
-
setDocumentContent(text);
|
2001 |
-
showToast(`Loaded XML file: ${file.name}`, 'success');
|
2002 |
-
hideLoadingOverlay();
|
2003 |
-
showStatus(`XML loaded: ${file.name}`, 'fa-file-alt');
|
2004 |
-
} catch (error) {
|
2005 |
-
showToast('Error parsing XML file', 'error');
|
2006 |
-
hideLoadingOverlay();
|
2007 |
-
showStatus('XML parsing failed', 'fa-exclamation-triangle');
|
2008 |
-
}
|
2009 |
-
};
|
2010 |
-
reader.onerror = () => {
|
2011 |
-
showToast('Error reading XML file', 'error');
|
2012 |
-
hideLoadingOverlay();
|
2013 |
-
showStatus('XML reading failed', 'fa-exclamation-triangle');
|
2014 |
-
};
|
2015 |
-
reader.readAsText(file);
|
2016 |
-
}
|
2017 |
-
|
2018 |
-
// CSV file reader
|
2019 |
-
function readCSVFile(file) {
|
2020 |
-
const reader = new FileReader();
|
2021 |
-
reader.onload = (e) => {
|
2022 |
-
const csvContent = e.target.result;
|
2023 |
-
|
2024 |
-
const lines = csvContent.split('\n').filter(line => line.trim());
|
2025 |
-
const headers = lines[0].split(',').map(h => h.trim().replace(/"/g, ''));
|
2026 |
-
|
2027 |
-
let text = `CSV Data Summary:\n\n`;
|
2028 |
-
text += `Headers: ${headers.join(', ')}\n\n`;
|
2029 |
-
text += `Total rows: ${lines.length - 1}\n\n`;
|
2030 |
-
|
2031 |
-
if (lines.length > 1) {
|
2032 |
-
text += 'First few rows:\n';
|
2033 |
-
for (let i = 1; i <= Math.min(5, lines.length - 1); i++) {
|
2034 |
-
const values = lines[i].split(',').map(v => v.trim().replace(/"/g, ''));
|
2035 |
-
text += `Row ${i}: ${values.join(', ')}\n`;
|
2036 |
-
}
|
2037 |
-
}
|
2038 |
-
|
2039 |
-
setDocumentContent(text);
|
2040 |
-
showToast('CSV file loaded. Showing data summary for TTS.', 'success');
|
2041 |
-
hideLoadingOverlay();
|
2042 |
-
showStatus(`CSV loaded: ${file.name}`, 'fa-file-alt');
|
2043 |
-
};
|
2044 |
-
reader.onerror = () => {
|
2045 |
-
showToast('Error reading CSV file', 'error');
|
2046 |
-
hideLoadingOverlay();
|
2047 |
-
showStatus('CSV reading failed', 'fa-exclamation-triangle');
|
2048 |
};
|
2049 |
reader.readAsText(file);
|
2050 |
}
|
@@ -2067,7 +2363,7 @@
|
|
2067 |
paragraphs.forEach((paragraph, index) => {
|
2068 |
const p = document.createElement('p');
|
2069 |
p.textContent = paragraph;
|
2070 |
-
p.classList.add('mb-2', 'leading-relaxed');
|
2071 |
p.dataset.paragraphIndex = index;
|
2072 |
|
2073 |
// Add click handler for paragraph selection
|
@@ -2162,126 +2458,38 @@
|
|
2162 |
'<i class="fas fa-bookmark mr-1"></i> Add Bookmark';
|
2163 |
});
|
2164 |
|
2165 |
-
// Enhanced synthesis with progress tracking
|
2166 |
-
async function performSynthesis() {
|
2167 |
-
if (!selectedVoice) {
|
2168 |
-
showToast('Please select a voice first', 'error');
|
2169 |
-
return null;
|
2170 |
-
}
|
2171 |
-
|
2172 |
-
if (!currentText.trim()) {
|
2173 |
-
showToast('Please add some text to synthesize', 'error');
|
2174 |
-
return null;
|
2175 |
-
}
|
2176 |
-
|
2177 |
-
isSynthesizing = true;
|
2178 |
-
updatePlaybackButtons();
|
2179 |
-
|
2180 |
-
try {
|
2181 |
-
showLoadingOverlay('Generating Speech', 'Preparing text chunks...');
|
2182 |
-
synthesisProgress.classList.remove('hidden');
|
2183 |
-
|
2184 |
-
const chunks = splitTextIntoChunks(currentText, MAX_CHUNK_SIZE);
|
2185 |
-
let allAudioData = [];
|
2186 |
-
|
2187 |
-
showStatus(`Synthesizing ${chunks.length} chunks...`, 'fa-magic', 'persistent');
|
2188 |
-
|
2189 |
-
for (let i = 0; i < chunks.length; i++) {
|
2190 |
-
const progress = (i / chunks.length) * 100;
|
2191 |
-
synthesisProgressBar.style.width = progress + '%';
|
2192 |
-
synthesisPercentage.textContent = Math.round(progress) + '%';
|
2193 |
-
loadingMessage.textContent = `Processing chunk ${i + 1} of ${chunks.length}...`;
|
2194 |
-
|
2195 |
-
const audioData = await synthesizeSpeech(
|
2196 |
-
chunks[i],
|
2197 |
-
selectedVoice,
|
2198 |
-
parseFloat(rateSelect.value),
|
2199 |
-
parseFloat(pitchSelect.value),
|
2200 |
-
modelSelect.value
|
2201 |
-
);
|
2202 |
-
|
2203 |
-
allAudioData.push(audioData);
|
2204 |
-
}
|
2205 |
-
|
2206 |
-
synthesisProgressBar.style.width = '100%';
|
2207 |
-
synthesisPercentage.textContent = '100%';
|
2208 |
-
|
2209 |
-
if (allAudioData.length > 0) {
|
2210 |
-
const combinedAudio = await combineAudioChunks(allAudioData);
|
2211 |
-
currentAudioBlob = combinedAudio;
|
2212 |
-
|
2213 |
-
showToast('Speech synthesis completed successfully', 'success');
|
2214 |
-
showStatus('Speech ready to play', 'fa-check');
|
2215 |
-
|
2216 |
-
return combinedAudio;
|
2217 |
-
}
|
2218 |
-
|
2219 |
-
} catch (error) {
|
2220 |
-
console.error('Error during synthesis:', error);
|
2221 |
-
showToast('Failed to generate speech: ' + error.message, 'error');
|
2222 |
-
showStatus('Synthesis failed', 'fa-exclamation-triangle');
|
2223 |
-
return null;
|
2224 |
-
} finally {
|
2225 |
-
isSynthesizing = false;
|
2226 |
-
updatePlaybackButtons();
|
2227 |
-
hideLoadingOverlay();
|
2228 |
-
synthesisProgress.classList.add('hidden');
|
2229 |
-
}
|
2230 |
-
}
|
2231 |
-
|
2232 |
-
// Combine audio chunks into a single blob
|
2233 |
-
async function combineAudioChunks(audioDataArray) {
|
2234 |
-
if (audioDataArray.length === 1) {
|
2235 |
-
const audioData = audioDataArray[0];
|
2236 |
-
const byteCharacters = atob(audioData);
|
2237 |
-
const byteNumbers = new Array(byteCharacters.length);
|
2238 |
-
for (let i = 0; i < byteCharacters.length; i++) {
|
2239 |
-
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
2240 |
-
}
|
2241 |
-
const byteArray = new Uint8Array(byteNumbers);
|
2242 |
-
return new Blob([byteArray], { type: 'audio/mpeg' });
|
2243 |
-
}
|
2244 |
-
|
2245 |
-
// For multiple chunks, use the first one as primary
|
2246 |
-
// (In a real implementation, you might want to properly concatenate audio)
|
2247 |
-
const firstAudioData = audioDataArray[0];
|
2248 |
-
const byteCharacters = atob(firstAudioData);
|
2249 |
-
const byteNumbers = new Array(byteCharacters.length);
|
2250 |
-
for (let i = 0; i < byteCharacters.length; i++) {
|
2251 |
-
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
2252 |
-
}
|
2253 |
-
const byteArray = new Uint8Array(byteNumbers);
|
2254 |
-
return new Blob([byteArray], { type: 'audio/mpeg' });
|
2255 |
-
}
|
2256 |
-
|
2257 |
// Playback controls
|
2258 |
synthesizeBtn.addEventListener('click', async () => {
|
2259 |
-
|
2260 |
-
if (audioBlob) {
|
2261 |
-
downloadBtn.disabled = false;
|
2262 |
-
saveToLibraryBtn.disabled = false;
|
2263 |
-
}
|
2264 |
});
|
2265 |
|
2266 |
playBtn.addEventListener('click', async () => {
|
2267 |
-
if (!currentAudioBlob && !isSynthesizing) {
|
2268 |
-
// No audio generated yet, synthesize first
|
2269 |
-
showToast('Generating speech first...', 'info');
|
2270 |
-
const audioBlob = await performSynthesis();
|
2271 |
-
if (!audioBlob) return;
|
2272 |
-
}
|
2273 |
-
|
2274 |
if (isPaused) {
|
2275 |
-
|
2276 |
-
|
2277 |
-
|
2278 |
} else if (isPlaying) {
|
2279 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2280 |
}
|
|
|
2281 |
});
|
2282 |
|
2283 |
stopBtn.addEventListener('click', () => {
|
2284 |
-
|
|
|
|
|
|
|
2285 |
});
|
2286 |
|
2287 |
downloadBtn.addEventListener('click', () => {
|
@@ -2289,90 +2497,13 @@
|
|
2289 |
});
|
2290 |
|
2291 |
// Enhanced playback functions
|
2292 |
-
async function startPlayback() {
|
2293 |
-
if (!currentAudioBlob || !audioContext) {
|
2294 |
-
showToast('No audio available to play', 'error');
|
2295 |
-
return;
|
2296 |
-
}
|
2297 |
-
|
2298 |
-
try {
|
2299 |
-
const arrayBuffer = await currentAudioBlob.arrayBuffer();
|
2300 |
-
audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
2301 |
-
|
2302 |
-
audioSource = audioContext.createBufferSource();
|
2303 |
-
audioSource.buffer = audioBuffer;
|
2304 |
-
audioSource.connect(audioContext.destination);
|
2305 |
-
|
2306 |
-
isPlaying = true;
|
2307 |
-
isPaused = false;
|
2308 |
-
updatePlaybackButtons();
|
2309 |
-
|
2310 |
-
audioSource.start(0);
|
2311 |
-
startTime = audioContext.currentTime;
|
2312 |
-
|
2313 |
-
startProgressTracking();
|
2314 |
-
showStatus('Playing audio', 'fa-play', 'persistent');
|
2315 |
-
|
2316 |
-
audioSource.onended = () => {
|
2317 |
-
if (isPlaying && !isPaused) {
|
2318 |
-
stopPlayback();
|
2319 |
-
}
|
2320 |
-
};
|
2321 |
-
} catch (error) {
|
2322 |
-
console.error('Error starting playback:', error);
|
2323 |
-
showToast('Failed to play audio: ' + error.message, 'error');
|
2324 |
-
}
|
2325 |
-
}
|
2326 |
-
|
2327 |
-
function pausePlayback() {
|
2328 |
-
if (audioSource && isPlaying && !isPaused) {
|
2329 |
-
pauseTime = audioContext.currentTime;
|
2330 |
-
audioSource.stop();
|
2331 |
-
isPaused = true;
|
2332 |
-
updatePlaybackButtons();
|
2333 |
-
showStatus('Audio paused', 'fa-pause');
|
2334 |
-
showToast('Playback paused', 'info', 1000);
|
2335 |
-
}
|
2336 |
-
}
|
2337 |
-
|
2338 |
-
function resumePlayback() {
|
2339 |
-
if (isPaused && audioBuffer) {
|
2340 |
-
const pausedDuration = pauseTime - startTime;
|
2341 |
-
|
2342 |
-
audioSource = audioContext.createBufferSource();
|
2343 |
-
audioSource.buffer = audioBuffer;
|
2344 |
-
audioSource.connect(audioContext.destination);
|
2345 |
-
|
2346 |
-
audioSource.start(0, pausedDuration);
|
2347 |
-
startTime = audioContext.currentTime - pausedDuration;
|
2348 |
-
isPaused = false;
|
2349 |
-
isPlaying = true;
|
2350 |
-
updatePlaybackButtons();
|
2351 |
-
|
2352 |
-
startProgressTracking();
|
2353 |
-
showStatus('Playing audio', 'fa-play', 'persistent');
|
2354 |
-
showToast('Playback resumed', 'info', 1000);
|
2355 |
-
|
2356 |
-
audioSource.onended = () => {
|
2357 |
-
if (isPlaying && !isPaused) {
|
2358 |
-
stopPlayback();
|
2359 |
-
}
|
2360 |
-
};
|
2361 |
-
}
|
2362 |
-
}
|
2363 |
-
|
2364 |
function stopPlayback() {
|
2365 |
-
if (audioSource && isPlaying) {
|
2366 |
-
audioSource.stop();
|
2367 |
-
}
|
2368 |
-
|
2369 |
isPlaying = false;
|
2370 |
isPaused = false;
|
2371 |
startTime = 0;
|
2372 |
pauseTime = 0;
|
2373 |
currentReadingPosition = 0;
|
2374 |
updatePlaybackButtons();
|
2375 |
-
updateProgress(0);
|
2376 |
|
2377 |
// Clear reading highlight
|
2378 |
documentContent.querySelectorAll('.reading-highlight').forEach(el => {
|
@@ -2380,7 +2511,6 @@
|
|
2380 |
});
|
2381 |
|
2382 |
showStatus('Audio stopped', 'fa-stop');
|
2383 |
-
showToast('Playback stopped', 'info', 1000);
|
2384 |
}
|
2385 |
|
2386 |
function updatePlaybackButtons() {
|
@@ -2398,62 +2528,12 @@
|
|
2398 |
playBtn.title = 'Play';
|
2399 |
}
|
2400 |
|
2401 |
-
playBtn.disabled = (
|
2402 |
-
stopBtn.disabled = !isPlaying;
|
2403 |
downloadBtn.disabled = !currentAudioBlob;
|
2404 |
saveToLibraryBtn.disabled = !currentAudioBlob;
|
2405 |
}
|
2406 |
|
2407 |
-
function updateProgress(progress) {
|
2408 |
-
progressBar.style.width = (progress * 100) + '%';
|
2409 |
-
progressBar.setAttribute('aria-valuenow', Math.round(progress * 100));
|
2410 |
-
|
2411 |
-
const currentSeconds = Math.floor(progress * estimatedDuration);
|
2412 |
-
const totalSeconds = Math.floor(estimatedDuration);
|
2413 |
-
|
2414 |
-
currentTime.textContent = formatTime(currentSeconds);
|
2415 |
-
totalTime.textContent = formatTime(totalSeconds);
|
2416 |
-
}
|
2417 |
-
|
2418 |
-
function splitTextIntoChunks(text, maxChunkSize) {
|
2419 |
-
const sentences = text.split(/(?<=[.!?])\s+/);
|
2420 |
-
const chunks = [];
|
2421 |
-
let currentChunk = '';
|
2422 |
-
|
2423 |
-
for (const sentence of sentences) {
|
2424 |
-
if (currentChunk.length + sentence.length <= maxChunkSize) {
|
2425 |
-
currentChunk += (currentChunk ? ' ' : '') + sentence;
|
2426 |
-
} else {
|
2427 |
-
if (currentChunk) {
|
2428 |
-
chunks.push(currentChunk.trim());
|
2429 |
-
}
|
2430 |
-
|
2431 |
-
// Handle very long sentences that exceed chunk size
|
2432 |
-
if (sentence.length > maxChunkSize) {
|
2433 |
-
const words = sentence.split(' ');
|
2434 |
-
let longChunk = '';
|
2435 |
-
for (const word of words) {
|
2436 |
-
if (longChunk.length + word.length + 1 <= maxChunkSize) {
|
2437 |
-
longChunk += (longChunk ? ' ' : '') + word;
|
2438 |
-
} else {
|
2439 |
-
if (longChunk) chunks.push(longChunk.trim());
|
2440 |
-
longChunk = word;
|
2441 |
-
}
|
2442 |
-
}
|
2443 |
-
currentChunk = longChunk;
|
2444 |
-
} else {
|
2445 |
-
currentChunk = sentence;
|
2446 |
-
}
|
2447 |
-
}
|
2448 |
-
}
|
2449 |
-
|
2450 |
-
if (currentChunk) {
|
2451 |
-
chunks.push(currentChunk.trim());
|
2452 |
-
}
|
2453 |
-
|
2454 |
-
return chunks;
|
2455 |
-
}
|
2456 |
-
|
2457 |
// Download functionality
|
2458 |
function downloadAudio() {
|
2459 |
if (!currentAudioBlob) {
|
@@ -2479,7 +2559,7 @@
|
|
2479 |
}
|
2480 |
}
|
2481 |
|
2482 |
-
// Audio Library Management
|
2483 |
libraryBtn.addEventListener('click', () => {
|
2484 |
openLibraryModal();
|
2485 |
});
|
@@ -2489,7 +2569,7 @@
|
|
2489 |
});
|
2490 |
|
2491 |
saveToLibraryBtn.addEventListener('click', () => {
|
2492 |
-
|
2493 |
});
|
2494 |
|
2495 |
clearLibraryBtn.addEventListener('click', () => {
|
@@ -2502,12 +2582,11 @@
|
|
2502 |
libraryModal.classList.remove('hidden');
|
2503 |
libraryModal.setAttribute('aria-hidden', 'false');
|
2504 |
updateLibraryView();
|
|
|
2505 |
|
2506 |
-
// Focus
|
2507 |
-
const
|
2508 |
-
if (
|
2509 |
-
focusableElements[0].focus();
|
2510 |
-
}
|
2511 |
}
|
2512 |
|
2513 |
function closeLibrary() {
|
@@ -2515,56 +2594,73 @@
|
|
2515 |
libraryModal.setAttribute('aria-hidden', 'true');
|
2516 |
}
|
2517 |
|
2518 |
-
|
2519 |
-
|
2520 |
-
|
2521 |
-
return;
|
2522 |
-
}
|
2523 |
|
2524 |
-
|
2525 |
-
|
2526 |
-
|
2527 |
-
|
2528 |
-
voice: selectedVoice ? selectedVoice.name : 'Unknown',
|
2529 |
-
settings: {
|
2530 |
-
rate: parseFloat(rateSelect.value),
|
2531 |
-
pitch: parseFloat(pitchSelect.value),
|
2532 |
-
model: modelSelect.value
|
2533 |
-
},
|
2534 |
-
date: new Date().toISOString(),
|
2535 |
-
size: currentAudioBlob.size
|
2536 |
-
};
|
2537 |
|
2538 |
-
|
2539 |
-
|
2540 |
-
|
2541 |
-
|
2542 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2543 |
|
2544 |
-
//
|
2545 |
-
|
2546 |
-
audioLibrary = audioLibrary.slice(-50);
|
2547 |
-
}
|
2548 |
|
2549 |
-
|
2550 |
-
|
2551 |
-
audioData: null // Don't store large audio data in localStorage
|
2552 |
-
}))));
|
2553 |
|
2554 |
-
|
2555 |
-
|
2556 |
-
|
2557 |
-
|
|
|
|
|
|
|
2558 |
}
|
2559 |
|
2560 |
function updateLibraryView() {
|
2561 |
libraryCount.textContent = `${audioLibrary.length} items`;
|
2562 |
-
|
2563 |
-
|
|
|
|
|
|
|
2564 |
libraryContent.innerHTML = `
|
2565 |
<div class="text-center py-8 text-gray-500 col-span-full">
|
2566 |
<i class="fas fa-music text-4xl mb-4" aria-hidden="true"></i>
|
2567 |
-
<p>No audio files
|
2568 |
<p class="text-sm mt-2">Generate and save audio to build your library</p>
|
2569 |
</div>
|
2570 |
`;
|
@@ -2573,34 +2669,61 @@
|
|
2573 |
|
2574 |
libraryContent.innerHTML = '';
|
2575 |
|
2576 |
-
|
2577 |
const itemEl = document.createElement('div');
|
2578 |
-
itemEl.className =
|
|
|
|
|
|
|
|
|
|
|
2579 |
itemEl.innerHTML = `
|
2580 |
-
<div class="flex justify-between items-start mb-
|
2581 |
-
<h3 class="font-medium text-gray-800 text-sm leading-tight">${item.title}</h3>
|
2582 |
-
<div class="flex gap-1
|
|
|
|
|
|
|
|
|
|
|
2583 |
<button class="load-library-item text-blue-500 hover:text-blue-700 p-1" data-id="${item.id}"
|
2584 |
title="Load this text" aria-label="Load text to editor">
|
2585 |
<i class="fas fa-upload text-xs" aria-hidden="true"></i>
|
2586 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2587 |
<button class="delete-library-item text-red-500 hover:text-red-700 p-1" data-id="${item.id}"
|
2588 |
-
title="Delete this item" aria-label="Delete
|
2589 |
<i class="fas fa-trash text-xs" aria-hidden="true"></i>
|
2590 |
</button>
|
2591 |
</div>
|
2592 |
</div>
|
|
|
2593 |
<div class="text-xs text-gray-600 mb-2">
|
2594 |
-
<div class="flex justify-between">
|
2595 |
<span><i class="fas fa-microphone mr-1" aria-hidden="true"></i> ${item.voice}</span>
|
2596 |
<span><i class="fas fa-clock mr-1" aria-hidden="true"></i> ${new Date(item.date).toLocaleDateString()}</span>
|
2597 |
</div>
|
2598 |
-
<div class="mt-1">
|
2599 |
<span class="bg-gray-100 px-2 py-1 rounded text-xs">
|
2600 |
${item.settings.rate}x speed, ${item.settings.pitch > 0 ? '+' : ''}${item.settings.pitch} pitch
|
2601 |
</span>
|
|
|
2602 |
</div>
|
2603 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2604 |
<p class="text-xs text-gray-500 line-clamp-2">${item.text.substring(0, 100)}...</p>
|
2605 |
`;
|
2606 |
|
@@ -2610,18 +2733,41 @@
|
|
2610 |
// Add event listeners to library items
|
2611 |
libraryContent.addEventListener('click', (e) => {
|
2612 |
const loadBtn = e.target.closest('.load-library-item');
|
|
|
|
|
2613 |
const deleteBtn = e.target.closest('.delete-library-item');
|
|
|
2614 |
|
2615 |
if (loadBtn) {
|
2616 |
const itemId = parseInt(loadBtn.dataset.id);
|
2617 |
loadLibraryItem(itemId);
|
|
|
|
|
|
|
|
|
|
|
|
|
2618 |
} else if (deleteBtn) {
|
2619 |
const itemId = parseInt(deleteBtn.dataset.id);
|
2620 |
deleteLibraryItem(itemId);
|
|
|
|
|
|
|
2621 |
}
|
2622 |
});
|
2623 |
}
|
2624 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2625 |
function loadLibraryItem(itemId) {
|
2626 |
const item = audioLibrary.find(i => i.id === itemId);
|
2627 |
if (!item) return;
|
@@ -2646,6 +2792,19 @@
|
|
2646 |
showStatus(`Loaded: ${item.title}`, 'fa-file-import');
|
2647 |
}
|
2648 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2649 |
function deleteLibraryItem(itemId) {
|
2650 |
if (confirm('Are you sure you want to delete this audio file?')) {
|
2651 |
audioLibrary = audioLibrary.filter(item => item.id !== itemId);
|
@@ -2655,6 +2814,16 @@
|
|
2655 |
}
|
2656 |
}
|
2657 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2658 |
function clearAudioLibrary() {
|
2659 |
audioLibrary = [];
|
2660 |
localStorage.setItem('audioLibrary', JSON.stringify([]));
|
@@ -2662,27 +2831,167 @@
|
|
2662 |
showToast('Audio library cleared', 'success');
|
2663 |
}
|
2664 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2665 |
// Keyboard shortcuts
|
2666 |
document.addEventListener('keydown', (e) => {
|
2667 |
// Ctrl+Space or Cmd+Space to play/pause
|
2668 |
if ((e.ctrlKey || e.metaKey) && e.code === 'Space') {
|
2669 |
e.preventDefault();
|
2670 |
-
|
2671 |
-
if (isPaused) {
|
2672 |
-
resumePlayback();
|
2673 |
-
} else {
|
2674 |
-
pausePlayback();
|
2675 |
-
}
|
2676 |
-
} else if (currentAudioBlob) {
|
2677 |
-
startPlayback();
|
2678 |
-
}
|
2679 |
}
|
2680 |
|
2681 |
-
// Ctrl+
|
|
|
|
|
|
|
|
|
|
|
|
|
2682 |
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
2683 |
e.preventDefault();
|
2684 |
-
if (
|
2685 |
-
|
2686 |
}
|
2687 |
}
|
2688 |
|
@@ -2690,6 +2999,8 @@
|
|
2690 |
if (e.key === 'Escape') {
|
2691 |
if (!libraryModal.classList.contains('hidden')) {
|
2692 |
closeLibrary();
|
|
|
|
|
2693 |
}
|
2694 |
}
|
2695 |
});
|
@@ -2705,6 +3016,12 @@
|
|
2705 |
}
|
2706 |
});
|
2707 |
|
|
|
|
|
|
|
|
|
|
|
|
|
2708 |
// Initialize the application
|
2709 |
init();
|
2710 |
});
|
|
|
3 |
<head>
|
4 |
<meta charset="UTF-8">
|
5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Advanced Google TTS Document Reader (BYOK)</title>
|
7 |
<script src="https://cdn.tailwindcss.com"></script>
|
8 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
9 |
<style>
|
|
|
200 |
transition: width 0.3s ease;
|
201 |
}
|
202 |
|
203 |
+
/* Chunk progress indicators */
|
204 |
+
.chunk-progress {
|
205 |
+
display: flex;
|
206 |
+
gap: 2px;
|
207 |
+
margin-top: 8px;
|
208 |
+
flex-wrap: wrap;
|
209 |
+
}
|
210 |
+
|
211 |
+
.chunk-indicator {
|
212 |
+
height: 6px;
|
213 |
+
min-width: 12px;
|
214 |
+
flex: 1;
|
215 |
+
background-color: var(--border-color);
|
216 |
+
border-radius: 3px;
|
217 |
+
transition: all 0.3s ease;
|
218 |
+
}
|
219 |
+
|
220 |
+
.chunk-indicator.synthesizing {
|
221 |
+
background-color: #F59E0B;
|
222 |
+
animation: pulse 1s infinite;
|
223 |
+
}
|
224 |
+
|
225 |
+
.chunk-indicator.completed {
|
226 |
+
background-color: #10B981;
|
227 |
+
}
|
228 |
+
|
229 |
+
.chunk-indicator.playing {
|
230 |
+
background-color: #3B82F6;
|
231 |
+
box-shadow: 0 0 4px rgba(59, 130, 246, 0.5);
|
232 |
+
}
|
233 |
+
|
234 |
+
@keyframes pulse {
|
235 |
+
0%, 100% { opacity: 1; }
|
236 |
+
50% { opacity: 0.5; }
|
237 |
+
}
|
238 |
+
|
239 |
/* Library styles */
|
240 |
.library-item {
|
241 |
border: 1px solid var(--border-color);
|
|
|
250 |
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
251 |
}
|
252 |
|
253 |
+
.library-item.favorited {
|
254 |
+
border-color: #F59E0B;
|
255 |
+
background-color: rgba(245, 158, 11, 0.05);
|
256 |
+
}
|
257 |
+
|
258 |
/* Modal styles */
|
259 |
.modal {
|
260 |
position: fixed;
|
|
|
323 |
opacity: 0;
|
324 |
}
|
325 |
|
326 |
+
/* Notes textarea */
|
327 |
+
.notes-textarea {
|
328 |
+
width: 100%;
|
329 |
+
min-height: 100px;
|
330 |
+
padding: 12px;
|
331 |
+
border: 1px solid var(--border-color);
|
332 |
+
border-radius: 8px;
|
333 |
+
background-color: var(--card-bg);
|
334 |
+
color: var(--text-primary);
|
335 |
+
resize: vertical;
|
336 |
+
}
|
337 |
+
|
338 |
+
.notes-textarea:focus {
|
339 |
+
outline: none;
|
340 |
+
ring: 2px solid #3B82F6;
|
341 |
+
border-color: #3B82F6;
|
342 |
+
}
|
343 |
+
|
344 |
+
/* Tags styling */
|
345 |
+
.tag {
|
346 |
+
display: inline-block;
|
347 |
+
background-color: #3B82F6;
|
348 |
+
color: white;
|
349 |
+
padding: 4px 8px;
|
350 |
+
border-radius: 4px;
|
351 |
+
font-size: 12px;
|
352 |
+
margin: 2px;
|
353 |
+
}
|
354 |
+
|
355 |
+
.tag.custom {
|
356 |
+
background-color: #10B981;
|
357 |
+
}
|
358 |
+
|
359 |
+
.tag-input {
|
360 |
+
display: inline-block;
|
361 |
+
min-width: 100px;
|
362 |
+
border: 1px dashed var(--border-color);
|
363 |
+
padding: 4px 8px;
|
364 |
+
border-radius: 4px;
|
365 |
+
font-size: 12px;
|
366 |
+
background: transparent;
|
367 |
+
}
|
368 |
+
|
369 |
+
/* Search and filter */
|
370 |
+
.filter-bar {
|
371 |
+
background-color: var(--card-bg);
|
372 |
+
border: 1px solid var(--border-color);
|
373 |
+
border-radius: 8px;
|
374 |
+
padding: 16px;
|
375 |
+
margin-bottom: 16px;
|
376 |
+
}
|
377 |
+
|
378 |
/* Accessibility improvements */
|
379 |
.sr-only {
|
380 |
position: absolute;
|
|
|
442 |
<body class="bg-gray-50 min-h-screen">
|
443 |
<div class="container mx-auto px-4 py-8">
|
444 |
<header class="text-center mb-8 relative">
|
445 |
+
<h1 class="text-4xl font-bold text-blue-600 mb-2">Advanced Google TTS Document Reader</h1>
|
446 |
+
<p class="text-gray-600">Natural HD voices with streaming synthesis and smart library</p>
|
447 |
|
448 |
<!-- Dark Mode Toggle -->
|
449 |
<button id="darkModeToggle" class="absolute top-0 right-0 p-2 text-gray-600 hover:text-gray-800 transition"
|
|
|
648 |
|
649 |
<div class="control-buttons flex items-center gap-2">
|
650 |
<button id="synthesizeBtn" class="bg-green-600 hover:bg-green-700 text-white rounded-full w-12 h-12 flex items-center justify-center transition"
|
651 |
+
title="Generate speech (starts playback when first chunk ready)" aria-label="Generate speech">
|
652 |
<i class="fas fa-magic" aria-hidden="true"></i>
|
653 |
</button>
|
654 |
<button id="playBtn" class="bg-blue-600 hover:bg-blue-700 text-white rounded-full w-12 h-12 flex items-center justify-center transition"
|
|
|
670 |
</div>
|
671 |
</div>
|
672 |
|
673 |
+
<!-- Chunk Progress -->
|
674 |
+
<div id="chunkProgress" class="mb-4 hidden">
|
675 |
+
<div class="flex justify-between text-sm text-gray-600 mb-2">
|
676 |
+
<span>Synthesis Progress</span>
|
677 |
+
<span id="chunkStatus">Starting...</span>
|
|
|
|
|
|
|
678 |
</div>
|
679 |
+
<div class="chunk-progress" id="chunkIndicators"></div>
|
680 |
</div>
|
681 |
|
682 |
+
<!-- Playback Progress -->
|
683 |
<div class="progress-container">
|
684 |
<div class="flex justify-between text-sm text-gray-600 mb-1">
|
685 |
<span id="currentTime" aria-live="polite">0:00</span>
|
|
|
716 |
</div>
|
717 |
</div>
|
718 |
|
719 |
+
<!-- Enhanced Audio Library Modal with Notes and Filing -->
|
720 |
<div id="libraryModal" class="modal hidden" role="dialog" aria-modal="true" aria-labelledby="libraryModalTitle">
|
721 |
+
<div class="modal-content max-w-6xl">
|
722 |
<div class="flex justify-between items-center mb-6">
|
723 |
<h2 id="libraryModalTitle" class="text-2xl font-semibold">Audio Library</h2>
|
724 |
<button id="closeLibraryModal" class="text-gray-400 hover:text-gray-600" aria-label="Close library">
|
|
|
726 |
</button>
|
727 |
</div>
|
728 |
|
729 |
+
<!-- Library Controls -->
|
730 |
+
<div class="mb-4 flex flex-col md:flex-row gap-4 justify-between items-start">
|
731 |
<div class="flex items-center gap-2">
|
732 |
<button id="saveToLibraryBtn" class="btn-primary" disabled aria-label="Save current audio to library">
|
733 |
<i class="fas fa-save mr-2" aria-hidden="true"></i> Save Current Audio
|
|
|
740 |
<div class="text-sm text-gray-500" id="libraryCount" aria-live="polite">0 items</div>
|
741 |
</div>
|
742 |
|
743 |
+
<!-- Search and Filter Bar -->
|
744 |
+
<div class="filter-bar">
|
745 |
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
746 |
+
<div>
|
747 |
+
<label for="librarySearch" class="block text-sm font-medium text-gray-700 mb-1">Search</label>
|
748 |
+
<input type="text" id="librarySearch" placeholder="Search titles, notes, or text..."
|
749 |
+
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
750 |
+
</div>
|
751 |
+
<div>
|
752 |
+
<label for="voiceFilter" class="block text-sm font-medium text-gray-700 mb-1">Voice Filter</label>
|
753 |
+
<select id="voiceFilter" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
754 |
+
<option value="">All Voices</option>
|
755 |
+
</select>
|
756 |
+
</div>
|
757 |
+
<div>
|
758 |
+
<label for="tagFilter" class="block text-sm font-medium text-gray-700 mb-1">Tag Filter</label>
|
759 |
+
<select id="tagFilter" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
760 |
+
<option value="">All Tags</option>
|
761 |
+
</select>
|
762 |
+
</div>
|
763 |
+
</div>
|
764 |
+
<div class="mt-3 flex gap-2 flex-wrap">
|
765 |
+
<button id="favoritesOnly" class="text-sm bg-yellow-100 hover:bg-yellow-200 px-3 py-1 rounded-md transition">
|
766 |
+
<i class="fas fa-star mr-1" aria-hidden="true"></i> Favorites Only
|
767 |
+
</button>
|
768 |
+
<button id="sortByDate" class="text-sm bg-gray-100 hover:bg-gray-200 px-3 py-1 rounded-md transition">
|
769 |
+
<i class="fas fa-calendar mr-1" aria-hidden="true"></i> Sort by Date
|
770 |
+
</button>
|
771 |
+
<button id="sortByDuration" class="text-sm bg-gray-100 hover:bg-gray-200 px-3 py-1 rounded-md transition">
|
772 |
+
<i class="fas fa-clock mr-1" aria-hidden="true"></i> Sort by Duration
|
773 |
+
</button>
|
774 |
+
</div>
|
775 |
+
</div>
|
776 |
+
|
777 |
+
<!-- Library Content Grid -->
|
778 |
+
<div id="libraryContent" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 max-h-96 overflow-y-auto">
|
779 |
<div class="text-center py-8 text-gray-500 col-span-full">
|
780 |
<i class="fas fa-music text-4xl mb-4" aria-hidden="true"></i>
|
781 |
<p>No audio files saved yet</p>
|
|
|
784 |
</div>
|
785 |
</div>
|
786 |
|
787 |
+
<!-- Notes Modal -->
|
788 |
+
<div id="notesModal" class="modal hidden" role="dialog" aria-modal="true" aria-labelledby="notesModalTitle">
|
789 |
+
<div class="modal-content max-w-2xl">
|
790 |
+
<div class="flex justify-between items-center mb-6">
|
791 |
+
<h2 id="notesModalTitle" class="text-xl font-semibold">Add Notes & Tags</h2>
|
792 |
+
<button id="closeNotesModal" class="text-gray-400 hover:text-gray-600" aria-label="Close notes">
|
793 |
+
<i class="fas fa-times text-xl" aria-hidden="true"></i>
|
794 |
+
</button>
|
795 |
+
</div>
|
796 |
+
|
797 |
+
<div class="space-y-4">
|
798 |
+
<div>
|
799 |
+
<label for="itemTitle" class="block text-sm font-medium text-gray-700 mb-1">Title</label>
|
800 |
+
<input type="text" id="itemTitle" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
801 |
+
</div>
|
802 |
+
|
803 |
+
<div>
|
804 |
+
<label for="itemNotes" class="block text-sm font-medium text-gray-700 mb-1">Notes</label>
|
805 |
+
<textarea id="itemNotes" placeholder="Add notes about this audio file..."
|
806 |
+
class="notes-textarea"></textarea>
|
807 |
+
</div>
|
808 |
+
|
809 |
+
<div>
|
810 |
+
<label class="block text-sm font-medium text-gray-700 mb-1">Tags</label>
|
811 |
+
<div id="tagContainer" class="border border-gray-300 rounded-md p-3 min-h-12">
|
812 |
+
<div id="currentTags" class="mb-2"></div>
|
813 |
+
<input type="text" id="tagInput" placeholder="Add tag..."
|
814 |
+
class="tag-input w-32 outline-none">
|
815 |
+
</div>
|
816 |
+
<div class="mt-2 text-xs text-gray-500">
|
817 |
+
<span class="font-medium">Suggested tags:</span>
|
818 |
+
<button class="tag-suggestion ml-1" data-tag="important">important</button>
|
819 |
+
<button class="tag-suggestion" data-tag="draft">draft</button>
|
820 |
+
<button class="tag-suggestion" data-tag="final">final</button>
|
821 |
+
<button class="tag-suggestion" data-tag="presentation">presentation</button>
|
822 |
+
<button class="tag-suggestion" data-tag="personal">personal</button>
|
823 |
+
</div>
|
824 |
+
</div>
|
825 |
+
</div>
|
826 |
+
|
827 |
+
<div class="mt-6 flex justify-end gap-3">
|
828 |
+
<button id="cancelNotes" class="bg-gray-300 hover:bg-gray-400 text-gray-700 py-2 px-4 rounded-lg transition">
|
829 |
+
Cancel
|
830 |
+
</button>
|
831 |
+
<button id="saveNotes" class="btn-primary">
|
832 |
+
<i class="fas fa-save mr-2" aria-hidden="true"></i> Save to Library
|
833 |
+
</button>
|
834 |
+
</div>
|
835 |
+
</div>
|
836 |
+
</div>
|
837 |
+
|
838 |
<!-- Required libraries for document processing -->
|
839 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.min.js"></script>
|
840 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/mammoth/1.4.21/mammoth.browser.min.js"></script>
|
|
|
860 |
let audioLibrary = JSON.parse(localStorage.getItem('audioLibrary') || '[]');
|
861 |
let bookmarks = JSON.parse(localStorage.getItem('documentBookmarks') || '[]');
|
862 |
|
863 |
+
// Chunk processing system
|
864 |
+
let chunkProcessor = null;
|
865 |
+
let currentChunks = [];
|
866 |
+
let completedChunks = [];
|
867 |
+
let playbackQueue = [];
|
868 |
+
let isStreamingPlayback = false;
|
869 |
+
let currentChunkIndex = 0;
|
870 |
+
|
871 |
const MAX_CHUNK_SIZE = 5000;
|
872 |
const PRICING = {
|
873 |
standard: 4.00,
|
|
|
926 |
const loadingOverlay = document.getElementById('loadingOverlay');
|
927 |
const loadingTitle = document.getElementById('loadingTitle');
|
928 |
const loadingMessage = document.getElementById('loadingMessage');
|
929 |
+
const chunkProgress = document.getElementById('chunkProgress');
|
930 |
+
const chunkStatus = document.getElementById('chunkStatus');
|
931 |
+
const chunkIndicators = document.getElementById('chunkIndicators');
|
932 |
|
933 |
// Library modal elements
|
934 |
const libraryModal = document.getElementById('libraryModal');
|
|
|
937 |
const clearLibraryBtn = document.getElementById('clearLibraryBtn');
|
938 |
const libraryContent = document.getElementById('libraryContent');
|
939 |
const libraryCount = document.getElementById('libraryCount');
|
940 |
+
const librarySearch = document.getElementById('librarySearch');
|
941 |
+
const voiceFilter = document.getElementById('voiceFilter');
|
942 |
+
const tagFilter = document.getElementById('tagFilter');
|
943 |
+
const favoritesOnly = document.getElementById('favoritesOnly');
|
944 |
+
const sortByDate = document.getElementById('sortByDate');
|
945 |
+
const sortByDuration = document.getElementById('sortByDuration');
|
946 |
+
|
947 |
+
// Notes modal elements
|
948 |
+
const notesModal = document.getElementById('notesModal');
|
949 |
+
const closeNotesModal = document.getElementById('closeNotesModal');
|
950 |
+
const itemTitle = document.getElementById('itemTitle');
|
951 |
+
const itemNotes = document.getElementById('itemNotes');
|
952 |
+
const tagContainer = document.getElementById('tagContainer');
|
953 |
+
const currentTags = document.getElementById('currentTags');
|
954 |
+
const tagInput = document.getElementById('tagInput');
|
955 |
+
const cancelNotes = document.getElementById('cancelNotes');
|
956 |
+
const saveNotes = document.getElementById('saveNotes');
|
957 |
|
958 |
// Dark mode elements
|
959 |
const darkModeToggle = document.getElementById('darkModeToggle');
|
960 |
|
961 |
+
// Chunk Processor Class
|
962 |
+
class ChunkProcessor {
|
963 |
+
constructor() {
|
964 |
+
this.chunks = [];
|
965 |
+
this.completedAudio = [];
|
966 |
+
this.synthesisQueue = [];
|
967 |
+
this.isProcessing = false;
|
968 |
+
this.onChunkComplete = null;
|
969 |
+
this.onAllComplete = null;
|
970 |
+
this.onProgress = null;
|
971 |
+
}
|
972 |
+
|
973 |
+
async processText(text, voice, settings) {
|
974 |
+
this.chunks = this.splitTextIntoChunks(text, MAX_CHUNK_SIZE);
|
975 |
+
this.completedAudio = new Array(this.chunks.length).fill(null);
|
976 |
+
this.synthesisQueue = this.chunks.map((chunk, index) => ({ chunk, index }));
|
977 |
+
this.isProcessing = true;
|
978 |
+
|
979 |
+
// Update chunk indicators
|
980 |
+
this.updateChunkIndicators();
|
981 |
+
|
982 |
+
// Process chunks in parallel (limited concurrency)
|
983 |
+
const concurrentLimit = 3;
|
984 |
+
const promises = [];
|
985 |
+
|
986 |
+
for (let i = 0; i < Math.min(concurrentLimit, this.synthesisQueue.length); i++) {
|
987 |
+
promises.push(this.processNextChunk(voice, settings));
|
988 |
+
}
|
989 |
+
|
990 |
+
await Promise.all(promises);
|
991 |
+
|
992 |
+
this.isProcessing = false;
|
993 |
+
if (this.onAllComplete) {
|
994 |
+
this.onAllComplete(this.completedAudio);
|
995 |
+
}
|
996 |
+
}
|
997 |
+
|
998 |
+
async processNextChunk(voice, settings) {
|
999 |
+
while (this.synthesisQueue.length > 0 && this.isProcessing) {
|
1000 |
+
const { chunk, index } = this.synthesisQueue.shift();
|
1001 |
+
|
1002 |
+
try {
|
1003 |
+
// Update chunk indicator to synthesizing
|
1004 |
+
const indicator = document.querySelectorAll('.chunk-indicator')[index];
|
1005 |
+
if (indicator) {
|
1006 |
+
indicator.classList.add('synthesizing');
|
1007 |
+
}
|
1008 |
+
|
1009 |
+
const audioData = await synthesizeSpeech(chunk, voice, settings.rate, settings.pitch, settings.model);
|
1010 |
+
this.completedAudio[index] = {
|
1011 |
+
audioData,
|
1012 |
+
chunk,
|
1013 |
+
index,
|
1014 |
+
timestamp: Date.now()
|
1015 |
+
};
|
1016 |
+
|
1017 |
+
// Update chunk indicator to completed
|
1018 |
+
if (indicator) {
|
1019 |
+
indicator.classList.remove('synthesizing');
|
1020 |
+
indicator.classList.add('completed');
|
1021 |
+
}
|
1022 |
+
|
1023 |
+
// Notify about chunk completion
|
1024 |
+
if (this.onChunkComplete) {
|
1025 |
+
this.onChunkComplete(this.completedAudio[index], index);
|
1026 |
+
}
|
1027 |
+
|
1028 |
+
// Update progress
|
1029 |
+
if (this.onProgress) {
|
1030 |
+
const completed = this.completedAudio.filter(chunk => chunk !== null).length;
|
1031 |
+
this.onProgress(completed, this.chunks.length);
|
1032 |
+
}
|
1033 |
+
|
1034 |
+
} catch (error) {
|
1035 |
+
console.error(`Error processing chunk ${index}:`, error);
|
1036 |
+
showToast(`Chunk ${index + 1} failed: ${error.message}`, 'error');
|
1037 |
+
|
1038 |
+
// Mark chunk as error
|
1039 |
+
const indicator = document.querySelectorAll('.chunk-indicator')[index];
|
1040 |
+
if (indicator) {
|
1041 |
+
indicator.classList.remove('synthesizing');
|
1042 |
+
indicator.style.backgroundColor = '#EF4444';
|
1043 |
+
}
|
1044 |
+
}
|
1045 |
+
}
|
1046 |
+
}
|
1047 |
+
|
1048 |
+
splitTextIntoChunks(text, maxChunkSize) {
|
1049 |
+
const sentences = text.split(/(?<=[.!?])\s+/);
|
1050 |
+
const chunks = [];
|
1051 |
+
let currentChunk = '';
|
1052 |
+
|
1053 |
+
for (const sentence of sentences) {
|
1054 |
+
if (currentChunk.length + sentence.length <= maxChunkSize) {
|
1055 |
+
currentChunk += (currentChunk ? ' ' : '') + sentence;
|
1056 |
+
} else {
|
1057 |
+
if (currentChunk) {
|
1058 |
+
chunks.push(currentChunk.trim());
|
1059 |
+
}
|
1060 |
+
|
1061 |
+
// Handle very long sentences
|
1062 |
+
if (sentence.length > maxChunkSize) {
|
1063 |
+
const words = sentence.split(' ');
|
1064 |
+
let longChunk = '';
|
1065 |
+
for (const word of words) {
|
1066 |
+
if (longChunk.length + word.length + 1 <= maxChunkSize) {
|
1067 |
+
longChunk += (longChunk ? ' ' : '') + word;
|
1068 |
+
} else {
|
1069 |
+
if (longChunk) chunks.push(longChunk.trim());
|
1070 |
+
longChunk = word;
|
1071 |
+
}
|
1072 |
+
}
|
1073 |
+
currentChunk = longChunk;
|
1074 |
+
} else {
|
1075 |
+
currentChunk = sentence;
|
1076 |
+
}
|
1077 |
+
}
|
1078 |
+
}
|
1079 |
+
|
1080 |
+
if (currentChunk) {
|
1081 |
+
chunks.push(currentChunk.trim());
|
1082 |
+
}
|
1083 |
+
|
1084 |
+
return chunks;
|
1085 |
+
}
|
1086 |
+
|
1087 |
+
updateChunkIndicators() {
|
1088 |
+
chunkIndicators.innerHTML = '';
|
1089 |
+
this.chunks.forEach((_, index) => {
|
1090 |
+
const indicator = document.createElement('div');
|
1091 |
+
indicator.className = 'chunk-indicator';
|
1092 |
+
indicator.title = `Chunk ${index + 1}`;
|
1093 |
+
chunkIndicators.appendChild(indicator);
|
1094 |
+
});
|
1095 |
+
}
|
1096 |
+
|
1097 |
+
stop() {
|
1098 |
+
this.isProcessing = false;
|
1099 |
+
this.synthesisQueue = [];
|
1100 |
+
}
|
1101 |
+
}
|
1102 |
+
|
1103 |
+
// Streaming Audio Player Class
|
1104 |
+
class StreamingAudioPlayer {
|
1105 |
+
constructor() {
|
1106 |
+
this.audioChunks = [];
|
1107 |
+
this.currentChunkIndex = 0;
|
1108 |
+
this.isPlaying = false;
|
1109 |
+
this.isPaused = false;
|
1110 |
+
this.totalDuration = 0;
|
1111 |
+
}
|
1112 |
+
|
1113 |
+
addChunk(audioData, index) {
|
1114 |
+
this.audioChunks[index] = audioData;
|
1115 |
+
|
1116 |
+
// Start playing if this is the first chunk and we're ready
|
1117 |
+
if (index === 0 && !this.isPlaying && isStreamingPlayback) {
|
1118 |
+
this.startPlayback();
|
1119 |
+
}
|
1120 |
+
}
|
1121 |
+
|
1122 |
+
async startPlayback() {
|
1123 |
+
if (this.audioChunks[0]) {
|
1124 |
+
this.isPlaying = true;
|
1125 |
+
this.currentChunkIndex = 0;
|
1126 |
+
await this.playChunk(0);
|
1127 |
+
}
|
1128 |
+
}
|
1129 |
+
|
1130 |
+
async playChunk(index) {
|
1131 |
+
if (!this.audioChunks[index] || !this.isPlaying) return;
|
1132 |
+
|
1133 |
+
try {
|
1134 |
+
const audioData = this.audioChunks[index].audioData;
|
1135 |
+
const arrayBuffer = Uint8Array.from(atob(audioData), c => c.charCodeAt(0)).buffer;
|
1136 |
+
audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
1137 |
+
|
1138 |
+
audioSource = audioContext.createBufferSource();
|
1139 |
+
audioSource.buffer = audioBuffer;
|
1140 |
+
audioSource.connect(audioContext.destination);
|
1141 |
+
|
1142 |
+
// Update chunk indicator to playing
|
1143 |
+
const indicators = document.querySelectorAll('.chunk-indicator');
|
1144 |
+
indicators.forEach(ind => ind.classList.remove('playing'));
|
1145 |
+
if (indicators[index]) {
|
1146 |
+
indicators[index].classList.add('playing');
|
1147 |
+
}
|
1148 |
+
|
1149 |
+
audioSource.start(0);
|
1150 |
+
startTime = audioContext.currentTime;
|
1151 |
+
|
1152 |
+
startProgressTracking();
|
1153 |
+
|
1154 |
+
audioSource.onended = () => {
|
1155 |
+
if (this.isPlaying && index < this.audioChunks.length - 1) {
|
1156 |
+
this.currentChunkIndex++;
|
1157 |
+
this.playChunk(this.currentChunkIndex);
|
1158 |
+
} else {
|
1159 |
+
this.stopPlayback();
|
1160 |
+
}
|
1161 |
+
};
|
1162 |
+
|
1163 |
+
} catch (error) {
|
1164 |
+
console.error('Error playing chunk:', error);
|
1165 |
+
showToast(`Error playing chunk ${index + 1}`, 'error');
|
1166 |
+
}
|
1167 |
+
}
|
1168 |
+
|
1169 |
+
pause() {
|
1170 |
+
if (audioSource && this.isPlaying && !this.isPaused) {
|
1171 |
+
pauseTime = audioContext.currentTime;
|
1172 |
+
audioSource.stop();
|
1173 |
+
this.isPaused = true;
|
1174 |
+
}
|
1175 |
+
}
|
1176 |
+
|
1177 |
+
resume() {
|
1178 |
+
if (this.isPaused) {
|
1179 |
+
this.isPaused = false;
|
1180 |
+
this.playChunk(this.currentChunkIndex);
|
1181 |
+
}
|
1182 |
+
}
|
1183 |
+
|
1184 |
+
stopPlayback() {
|
1185 |
+
this.isPlaying = false;
|
1186 |
+
this.isPaused = false;
|
1187 |
+
this.currentChunkIndex = 0;
|
1188 |
+
if (audioSource) {
|
1189 |
+
audioSource.stop();
|
1190 |
+
}
|
1191 |
+
|
1192 |
+
// Clear playing indicators
|
1193 |
+
document.querySelectorAll('.chunk-indicator').forEach(ind => {
|
1194 |
+
ind.classList.remove('playing');
|
1195 |
+
});
|
1196 |
+
|
1197 |
+
stopPlayback();
|
1198 |
+
}
|
1199 |
+
}
|
1200 |
+
|
1201 |
+
// Initialize streaming player
|
1202 |
+
const streamingPlayer = new StreamingAudioPlayer();
|
1203 |
+
|
1204 |
// Utility function for file size formatting
|
1205 |
function formatFileSize(bytes) {
|
1206 |
if (bytes === 0) return '0 Bytes';
|
|
|
1280 |
// Initialize library
|
1281 |
updateLibraryView();
|
1282 |
|
1283 |
+
// Initialize library filtering
|
1284 |
+
initLibraryFiltering();
|
1285 |
+
|
1286 |
+
// Initialize notes system
|
1287 |
+
initNotesSystem();
|
1288 |
+
|
1289 |
// Event listeners
|
1290 |
modelSelect.addEventListener('change', (e) => {
|
1291 |
updateCostEstimator();
|
|
|
1748 |
// Store all voices and populate dropdown
|
1749 |
availableVoices = data.voices.sort((a, b) => a.name.localeCompare(b.name));
|
1750 |
populateVoiceDropdown(availableVoices);
|
1751 |
+
populateVoiceFilter(availableVoices);
|
1752 |
|
1753 |
// Auto-select standard voice by default
|
1754 |
autoSelectVoiceForModel('standard');
|
|
|
1819 |
});
|
1820 |
}
|
1821 |
|
1822 |
+
// Populate voice filter for library
|
1823 |
+
function populateVoiceFilter(voices) {
|
1824 |
+
voiceFilter.innerHTML = '<option value="">All Voices</option>';
|
1825 |
+
const uniqueVoices = [...new Set(voices.map(v => v.name))].sort();
|
1826 |
+
uniqueVoices.forEach(voice => {
|
1827 |
+
const option = document.createElement('option');
|
1828 |
+
option.value = voice;
|
1829 |
+
option.textContent = voice;
|
1830 |
+
voiceFilter.appendChild(option);
|
1831 |
+
});
|
1832 |
+
}
|
1833 |
+
|
1834 |
+
// Enhanced synthesize speech
|
1835 |
async function synthesizeSpeech(text, voice, rate = 1, pitch = 0, model = 'standard') {
|
1836 |
if (!apiKey) {
|
1837 |
throw new Error('API key not provided');
|
|
|
1927 |
throw new Error('Invalid response from Google TTS API - no audio content received');
|
1928 |
}
|
1929 |
|
|
|
1930 |
return data.audioContent;
|
1931 |
} catch (error) {
|
1932 |
console.error('Synthesis error:', error);
|
|
|
1934 |
}
|
1935 |
}
|
1936 |
|
1937 |
+
// Start streaming synthesis and playback
|
1938 |
+
async function startStreamingSynthesis() {
|
1939 |
+
if (!selectedVoice) {
|
1940 |
+
showToast('Please select a voice first', 'error');
|
1941 |
+
return;
|
1942 |
}
|
1943 |
|
1944 |
+
if (!currentText.trim()) {
|
1945 |
+
showToast('Please add some text to synthesize', 'error');
|
1946 |
+
return;
|
1947 |
+
}
|
1948 |
+
|
1949 |
+
isSynthesizing = true;
|
1950 |
+
isStreamingPlayback = true;
|
1951 |
+
updatePlaybackButtons();
|
1952 |
+
|
1953 |
+
// Show chunk progress
|
1954 |
+
chunkProgress.classList.remove('hidden');
|
1955 |
+
|
1956 |
+
// Initialize chunk processor
|
1957 |
+
chunkProcessor = new ChunkProcessor();
|
1958 |
+
|
1959 |
+
// Set up callbacks
|
1960 |
+
chunkProcessor.onChunkComplete = (audioChunk, index) => {
|
1961 |
+
// Add to streaming player
|
1962 |
+
streamingPlayer.addChunk(audioChunk, index);
|
1963 |
|
1964 |
+
// Update status
|
1965 |
+
const completed = chunkProcessor.completedAudio.filter(chunk => chunk !== null).length;
|
1966 |
+
chunkStatus.textContent = `${completed}/${chunkProcessor.chunks.length} chunks completed`;
|
1967 |
+
};
|
1968 |
+
|
1969 |
+
chunkProcessor.onProgress = (completed, total) => {
|
1970 |
+
chunkStatus.textContent = `${completed}/${total} chunks completed`;
|
1971 |
+
};
|
1972 |
+
|
1973 |
+
chunkProcessor.onAllComplete = (allAudio) => {
|
1974 |
+
isSynthesizing = false;
|
1975 |
updatePlaybackButtons();
|
1976 |
+
showToast('All chunks synthesized successfully', 'success');
|
1977 |
+
showStatus('Speech synthesis complete', 'fa-check');
|
1978 |
|
1979 |
+
// Prepare final audio blob for download
|
1980 |
+
prepareFinalAudioBlob(allAudio);
|
1981 |
+
};
|
1982 |
+
|
1983 |
+
// Start processing
|
1984 |
+
const settings = {
|
1985 |
+
rate: parseFloat(rateSelect.value),
|
1986 |
+
pitch: parseFloat(pitchSelect.value),
|
1987 |
+
model: modelSelect.value
|
1988 |
+
};
|
1989 |
+
|
1990 |
+
showStatus('Starting synthesis...', 'fa-magic', 'persistent');
|
1991 |
+
|
1992 |
+
try {
|
1993 |
+
await chunkProcessor.processText(currentText, selectedVoice, settings);
|
1994 |
+
} catch (error) {
|
1995 |
+
console.error('Error during streaming synthesis:', error);
|
1996 |
+
showToast('Synthesis failed: ' + error.message, 'error');
|
1997 |
+
stopStreamingSynthesis();
|
1998 |
+
}
|
1999 |
+
}
|
2000 |
+
|
2001 |
+
function stopStreamingSynthesis() {
|
2002 |
+
if (chunkProcessor) {
|
2003 |
+
chunkProcessor.stop();
|
2004 |
+
}
|
2005 |
+
streamingPlayer.stopPlayback();
|
2006 |
+
isSynthesizing = false;
|
2007 |
+
isStreamingPlayback = false;
|
2008 |
+
updatePlaybackButtons();
|
2009 |
+
chunkProgress.classList.add('hidden');
|
2010 |
+
showStatus('Synthesis stopped', 'fa-stop');
|
2011 |
+
}
|
2012 |
+
|
2013 |
+
// Prepare final audio blob from all chunks
|
2014 |
+
async function prepareFinalAudioBlob(audioChunks) {
|
2015 |
+
try {
|
2016 |
+
// For now, use the first chunk as the primary audio
|
2017 |
+
// In a full implementation, you would concatenate all chunks
|
2018 |
+
if (audioChunks.length > 0 && audioChunks[0]) {
|
2019 |
+
const firstAudioData = audioChunks[0].audioData;
|
2020 |
+
const byteCharacters = atob(firstAudioData);
|
2021 |
+
const byteNumbers = new Array(byteCharacters.length);
|
2022 |
+
for (let i = 0; i < byteCharacters.length; i++) {
|
2023 |
+
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
2024 |
}
|
2025 |
+
const byteArray = new Uint8Array(byteNumbers);
|
2026 |
+
currentAudioBlob = new Blob([byteArray], { type: 'audio/mpeg' });
|
2027 |
+
|
2028 |
+
downloadBtn.disabled = false;
|
2029 |
+
saveToLibraryBtn.disabled = false;
|
2030 |
+
}
|
2031 |
} catch (error) {
|
2032 |
+
console.error('Error preparing final audio:', error);
|
2033 |
+
showToast('Error preparing audio for download', 'error');
|
2034 |
}
|
2035 |
}
|
2036 |
|
2037 |
+
// Progress tracking
|
2038 |
function startProgressTracking() {
|
2039 |
function updateProgress() {
|
2040 |
if (!isPlaying || isPaused || !audioSource) return;
|
|
|
2234 |
}
|
2235 |
}
|
2236 |
|
2237 |
+
// File readers (streamlined for space)
|
2238 |
function readTextFile(file) {
|
2239 |
const reader = new FileReader();
|
2240 |
reader.onload = (e) => {
|
2241 |
+
setDocumentContent(e.target.result);
|
|
|
2242 |
showToast(`Loaded ${formatFileSize(file.size)} text file`, 'success');
|
2243 |
hideLoadingOverlay();
|
|
|
2244 |
};
|
2245 |
reader.onerror = () => {
|
2246 |
showToast('Error reading text file', 'error');
|
2247 |
hideLoadingOverlay();
|
|
|
2248 |
};
|
2249 |
reader.readAsText(file, 'UTF-8');
|
2250 |
}
|
2251 |
|
|
|
2252 |
function readPDFFile(file) {
|
2253 |
if (typeof pdfjsLib === 'undefined') {
|
2254 |
showToast('PDF.js not loaded. Please refresh the page and try again.', 'error');
|
|
|
2257 |
}
|
2258 |
|
2259 |
const fileURL = URL.createObjectURL(file);
|
2260 |
+
const loadingTask = pdfjsLib.getDocument({ url: fileURL, verbosity: 0 });
|
|
|
|
|
|
|
|
|
2261 |
|
2262 |
loadingTask.promise.then(pdf => {
|
|
|
|
|
2263 |
const pagePromises = [];
|
2264 |
+
for (let i = 1; i <= pdf.numPages; i++) {
|
2265 |
+
pagePromises.push(pdf.getPage(i).then(page =>
|
2266 |
+
page.getTextContent().then(textContent =>
|
2267 |
+
textContent.items.map(item => item.str || '').join(' ')
|
2268 |
+
)
|
2269 |
+
));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2270 |
}
|
2271 |
|
2272 |
Promise.all(pagePromises).then(pagesText => {
|
2273 |
+
setDocumentContent(pagesText.join('\n\n'));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2274 |
URL.revokeObjectURL(fileURL);
|
2275 |
+
showToast(`Loaded PDF with ${pdf.numPages} pages`, 'success');
|
2276 |
hideLoadingOverlay();
|
|
|
2277 |
});
|
2278 |
}).catch(error => {
|
2279 |
console.error('Error loading PDF:', error);
|
2280 |
showToast('Failed to load PDF file: ' + error.message, 'error');
|
|
|
2281 |
hideLoadingOverlay();
|
|
|
2282 |
});
|
2283 |
}
|
2284 |
|
|
|
2285 |
function readWordDocument(file) {
|
2286 |
if (typeof mammoth === 'undefined') {
|
2287 |
showToast('Mammoth.js not loaded. Please refresh the page and try again.', 'error');
|
|
|
2292 |
const reader = new FileReader();
|
2293 |
reader.onload = async (e) => {
|
2294 |
try {
|
2295 |
+
const result = await mammoth.extractRawText({ arrayBuffer: e.target.result });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2296 |
if (result.value && result.value.trim()) {
|
2297 |
setDocumentContent(result.value);
|
2298 |
showToast(`Loaded Word document: ${file.name}`, 'success');
|
|
|
2299 |
} else {
|
2300 |
showToast('No text content found in Word document', 'error');
|
|
|
|
|
|
|
|
|
|
|
2301 |
}
|
2302 |
} catch (error) {
|
2303 |
console.error('Error reading Word document:', error);
|
2304 |
+
showToast('Error reading Word document', 'error');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2305 |
} finally {
|
2306 |
hideLoadingOverlay();
|
2307 |
}
|
2308 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2309 |
reader.readAsArrayBuffer(file);
|
2310 |
}
|
2311 |
|
2312 |
+
// Additional file readers - simplified for space
|
2313 |
+
function readMarkdownFile(file) {
|
2314 |
const reader = new FileReader();
|
2315 |
reader.onload = (e) => {
|
2316 |
+
let text = e.target.result;
|
2317 |
+
// Basic Markdown to plain text conversion
|
2318 |
+
text = text.replace(/^#{1,6}\s+/gm, '');
|
2319 |
+
text = text.replace(/\*\*(.*?)\*\*/g, '$1');
|
2320 |
+
text = text.replace(/\*(.*?)\*/g, '$1');
|
2321 |
+
text = text.replace(/`(.*?)`/g, '$1');
|
2322 |
+
text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
|
2323 |
+
setDocumentContent(text);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2324 |
hideLoadingOverlay();
|
|
|
2325 |
};
|
2326 |
reader.readAsText(file);
|
2327 |
}
|
2328 |
|
2329 |
+
function readCSVFile(file) {
|
|
|
2330 |
const reader = new FileReader();
|
2331 |
reader.onload = (e) => {
|
2332 |
+
const lines = e.target.result.split('\n').filter(line => line.trim());
|
2333 |
+
const headers = lines[0].split(',').map(h => h.trim().replace(/"/g, ''));
|
2334 |
+
let text = `CSV Data Summary:\n\nHeaders: ${headers.join(', ')}\n\nTotal rows: ${lines.length - 1}\n\n`;
|
2335 |
+
if (lines.length > 1) {
|
2336 |
+
text += 'First few rows:\n';
|
2337 |
+
for (let i = 1; i <= Math.min(5, lines.length - 1); i++) {
|
2338 |
+
const values = lines[i].split(',').map(v => v.trim().replace(/"/g, ''));
|
2339 |
+
text += `Row ${i}: ${values.join(', ')}\n`;
|
2340 |
+
}
|
2341 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2342 |
setDocumentContent(text);
|
|
|
|
|
|
|
|
|
|
|
|
|
2343 |
hideLoadingOverlay();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2344 |
};
|
2345 |
reader.readAsText(file);
|
2346 |
}
|
|
|
2363 |
paragraphs.forEach((paragraph, index) => {
|
2364 |
const p = document.createElement('p');
|
2365 |
p.textContent = paragraph;
|
2366 |
+
p.classList.add('mb-2', 'leading-relaxed', 'cursor-pointer', 'hover:bg-gray-50', 'transition', 'p-2', 'rounded');
|
2367 |
p.dataset.paragraphIndex = index;
|
2368 |
|
2369 |
// Add click handler for paragraph selection
|
|
|
2458 |
'<i class="fas fa-bookmark mr-1"></i> Add Bookmark';
|
2459 |
});
|
2460 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2461 |
// Playback controls
|
2462 |
synthesizeBtn.addEventListener('click', async () => {
|
2463 |
+
await startStreamingSynthesis();
|
|
|
|
|
|
|
|
|
2464 |
});
|
2465 |
|
2466 |
playBtn.addEventListener('click', async () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2467 |
if (isPaused) {
|
2468 |
+
streamingPlayer.resume();
|
2469 |
+
isPlaying = true;
|
2470 |
+
isPaused = false;
|
2471 |
} else if (isPlaying) {
|
2472 |
+
streamingPlayer.pause();
|
2473 |
+
isPlaying = false;
|
2474 |
+
isPaused = true;
|
2475 |
+
} else {
|
2476 |
+
// Start new playback or resume streaming
|
2477 |
+
if (streamingPlayer.audioChunks.length > 0) {
|
2478 |
+
await streamingPlayer.startPlayback();
|
2479 |
+
isPlaying = true;
|
2480 |
+
isPaused = false;
|
2481 |
+
} else if (!isSynthesizing) {
|
2482 |
+
await startStreamingSynthesis();
|
2483 |
+
}
|
2484 |
}
|
2485 |
+
updatePlaybackButtons();
|
2486 |
});
|
2487 |
|
2488 |
stopBtn.addEventListener('click', () => {
|
2489 |
+
stopStreamingSynthesis();
|
2490 |
+
isPlaying = false;
|
2491 |
+
isPaused = false;
|
2492 |
+
updatePlaybackButtons();
|
2493 |
});
|
2494 |
|
2495 |
downloadBtn.addEventListener('click', () => {
|
|
|
2497 |
});
|
2498 |
|
2499 |
// Enhanced playback functions
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2500 |
function stopPlayback() {
|
|
|
|
|
|
|
|
|
2501 |
isPlaying = false;
|
2502 |
isPaused = false;
|
2503 |
startTime = 0;
|
2504 |
pauseTime = 0;
|
2505 |
currentReadingPosition = 0;
|
2506 |
updatePlaybackButtons();
|
|
|
2507 |
|
2508 |
// Clear reading highlight
|
2509 |
documentContent.querySelectorAll('.reading-highlight').forEach(el => {
|
|
|
2511 |
});
|
2512 |
|
2513 |
showStatus('Audio stopped', 'fa-stop');
|
|
|
2514 |
}
|
2515 |
|
2516 |
function updatePlaybackButtons() {
|
|
|
2528 |
playBtn.title = 'Play';
|
2529 |
}
|
2530 |
|
2531 |
+
playBtn.disabled = !currentText.trim() || !selectedVoice;
|
2532 |
+
stopBtn.disabled = !isPlaying && !isSynthesizing;
|
2533 |
downloadBtn.disabled = !currentAudioBlob;
|
2534 |
saveToLibraryBtn.disabled = !currentAudioBlob;
|
2535 |
}
|
2536 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2537 |
// Download functionality
|
2538 |
function downloadAudio() {
|
2539 |
if (!currentAudioBlob) {
|
|
|
2559 |
}
|
2560 |
}
|
2561 |
|
2562 |
+
// Enhanced Audio Library Management with Notes and Filing
|
2563 |
libraryBtn.addEventListener('click', () => {
|
2564 |
openLibraryModal();
|
2565 |
});
|
|
|
2569 |
});
|
2570 |
|
2571 |
saveToLibraryBtn.addEventListener('click', () => {
|
2572 |
+
openNotesModal();
|
2573 |
});
|
2574 |
|
2575 |
clearLibraryBtn.addEventListener('click', () => {
|
|
|
2582 |
libraryModal.classList.remove('hidden');
|
2583 |
libraryModal.setAttribute('aria-hidden', 'false');
|
2584 |
updateLibraryView();
|
2585 |
+
populateTagFilter();
|
2586 |
|
2587 |
+
// Focus first element
|
2588 |
+
const firstFocusable = libraryModal.querySelector('button, input, select');
|
2589 |
+
if (firstFocusable) firstFocusable.focus();
|
|
|
|
|
2590 |
}
|
2591 |
|
2592 |
function closeLibrary() {
|
|
|
2594 |
libraryModal.setAttribute('aria-hidden', 'true');
|
2595 |
}
|
2596 |
|
2597 |
+
// Initialize library filtering
|
2598 |
+
function initLibraryFiltering() {
|
2599 |
+
let filterTimeout;
|
|
|
|
|
2600 |
|
2601 |
+
librarySearch.addEventListener('input', () => {
|
2602 |
+
clearTimeout(filterTimeout);
|
2603 |
+
filterTimeout = setTimeout(filterLibrary, 300);
|
2604 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2605 |
|
2606 |
+
voiceFilter.addEventListener('change', filterLibrary);
|
2607 |
+
tagFilter.addEventListener('change', filterLibrary);
|
2608 |
+
|
2609 |
+
favoritesOnly.addEventListener('click', (e) => {
|
2610 |
+
e.target.classList.toggle('bg-yellow-200');
|
2611 |
+
filterLibrary();
|
2612 |
+
});
|
2613 |
+
|
2614 |
+
sortByDate.addEventListener('click', () => {
|
2615 |
+
audioLibrary.sort((a, b) => new Date(b.date) - new Date(a.date));
|
2616 |
+
updateLibraryView();
|
2617 |
+
});
|
2618 |
+
|
2619 |
+
sortByDuration.addEventListener('click', () => {
|
2620 |
+
audioLibrary.sort((a, b) => (b.duration || 0) - (a.duration || 0));
|
2621 |
+
updateLibraryView();
|
2622 |
+
});
|
2623 |
+
}
|
2624 |
+
|
2625 |
+
function filterLibrary() {
|
2626 |
+
const searchTerm = librarySearch.value.toLowerCase();
|
2627 |
+
const voiceFilter = document.getElementById('voiceFilter').value;
|
2628 |
+
const tagFilter = document.getElementById('tagFilter').value;
|
2629 |
+
const showFavoritesOnly = favoritesOnly.classList.contains('bg-yellow-200');
|
2630 |
+
|
2631 |
+
let filteredItems = audioLibrary.filter(item => {
|
2632 |
+
// Search filter
|
2633 |
+
const searchMatch = !searchTerm ||
|
2634 |
+
item.title.toLowerCase().includes(searchTerm) ||
|
2635 |
+
item.notes?.toLowerCase().includes(searchTerm) ||
|
2636 |
+
item.text.toLowerCase().includes(searchTerm);
|
2637 |
|
2638 |
+
// Voice filter
|
2639 |
+
const voiceMatch = !voiceFilter || item.voice === voiceFilter;
|
|
|
|
|
2640 |
|
2641 |
+
// Tag filter
|
2642 |
+
const tagMatch = !tagFilter || (item.tags && item.tags.includes(tagFilter));
|
|
|
|
|
2643 |
|
2644 |
+
// Favorites filter
|
2645 |
+
const favoriteMatch = !showFavoritesOnly || item.isFavorite;
|
2646 |
+
|
2647 |
+
return searchMatch && voiceMatch && tagMatch && favoriteMatch;
|
2648 |
+
});
|
2649 |
+
|
2650 |
+
renderLibraryItems(filteredItems);
|
2651 |
}
|
2652 |
|
2653 |
function updateLibraryView() {
|
2654 |
libraryCount.textContent = `${audioLibrary.length} items`;
|
2655 |
+
renderLibraryItems(audioLibrary);
|
2656 |
+
}
|
2657 |
+
|
2658 |
+
function renderLibraryItems(items) {
|
2659 |
+
if (items.length === 0) {
|
2660 |
libraryContent.innerHTML = `
|
2661 |
<div class="text-center py-8 text-gray-500 col-span-full">
|
2662 |
<i class="fas fa-music text-4xl mb-4" aria-hidden="true"></i>
|
2663 |
+
<p>No audio files found</p>
|
2664 |
<p class="text-sm mt-2">Generate and save audio to build your library</p>
|
2665 |
</div>
|
2666 |
`;
|
|
|
2669 |
|
2670 |
libraryContent.innerHTML = '';
|
2671 |
|
2672 |
+
items.forEach(item => {
|
2673 |
const itemEl = document.createElement('div');
|
2674 |
+
itemEl.className = `library-item ${item.isFavorite ? 'favorited' : ''}`;
|
2675 |
+
|
2676 |
+
const tagsHtml = item.tags ? item.tags.map(tag =>
|
2677 |
+
`<span class="tag">${tag}</span>`
|
2678 |
+
).join('') : '';
|
2679 |
+
|
2680 |
itemEl.innerHTML = `
|
2681 |
+
<div class="flex justify-between items-start mb-3">
|
2682 |
+
<h3 class="font-medium text-gray-800 text-sm leading-tight flex-1 mr-2">${item.title}</h3>
|
2683 |
+
<div class="flex gap-1">
|
2684 |
+
<button class="toggle-favorite text-yellow-500 hover:text-yellow-700 p-1" data-id="${item.id}"
|
2685 |
+
title="${item.isFavorite ? 'Remove from favorites' : 'Add to favorites'}"
|
2686 |
+
aria-label="Toggle favorite">
|
2687 |
+
<i class="fas fa-star${item.isFavorite ? '' : '-o'} text-xs" aria-hidden="true"></i>
|
2688 |
+
</button>
|
2689 |
<button class="load-library-item text-blue-500 hover:text-blue-700 p-1" data-id="${item.id}"
|
2690 |
title="Load this text" aria-label="Load text to editor">
|
2691 |
<i class="fas fa-upload text-xs" aria-hidden="true"></i>
|
2692 |
</button>
|
2693 |
+
<button class="play-library-item text-green-500 hover:text-green-700 p-1" data-id="${item.id}"
|
2694 |
+
title="Play this audio" aria-label="Play audio">
|
2695 |
+
<i class="fas fa-play text-xs" aria-hidden="true"></i>
|
2696 |
+
</button>
|
2697 |
+
<button class="edit-library-item text-purple-500 hover:text-purple-700 p-1" data-id="${item.id}"
|
2698 |
+
title="Edit notes and tags" aria-label="Edit item">
|
2699 |
+
<i class="fas fa-edit text-xs" aria-hidden="true"></i>
|
2700 |
+
</button>
|
2701 |
<button class="delete-library-item text-red-500 hover:text-red-700 p-1" data-id="${item.id}"
|
2702 |
+
title="Delete this item" aria-label="Delete item">
|
2703 |
<i class="fas fa-trash text-xs" aria-hidden="true"></i>
|
2704 |
</button>
|
2705 |
</div>
|
2706 |
</div>
|
2707 |
+
|
2708 |
<div class="text-xs text-gray-600 mb-2">
|
2709 |
+
<div class="flex justify-between items-center">
|
2710 |
<span><i class="fas fa-microphone mr-1" aria-hidden="true"></i> ${item.voice}</span>
|
2711 |
<span><i class="fas fa-clock mr-1" aria-hidden="true"></i> ${new Date(item.date).toLocaleDateString()}</span>
|
2712 |
</div>
|
2713 |
+
<div class="mt-1 flex items-center justify-between">
|
2714 |
<span class="bg-gray-100 px-2 py-1 rounded text-xs">
|
2715 |
${item.settings.rate}x speed, ${item.settings.pitch > 0 ? '+' : ''}${item.settings.pitch} pitch
|
2716 |
</span>
|
2717 |
+
${item.duration ? `<span class="text-xs">${formatTime(Math.floor(item.duration))}</span>` : ''}
|
2718 |
</div>
|
2719 |
</div>
|
2720 |
+
|
2721 |
+
${item.notes ? `<div class="text-xs text-gray-700 bg-gray-50 p-2 rounded mb-2">"${item.notes}"</div>` : ''}
|
2722 |
+
|
2723 |
+
<div class="flex flex-wrap gap-1 mb-2">
|
2724 |
+
${tagsHtml}
|
2725 |
+
</div>
|
2726 |
+
|
2727 |
<p class="text-xs text-gray-500 line-clamp-2">${item.text.substring(0, 100)}...</p>
|
2728 |
`;
|
2729 |
|
|
|
2733 |
// Add event listeners to library items
|
2734 |
libraryContent.addEventListener('click', (e) => {
|
2735 |
const loadBtn = e.target.closest('.load-library-item');
|
2736 |
+
const playBtn = e.target.closest('.play-library-item');
|
2737 |
+
const editBtn = e.target.closest('.edit-library-item');
|
2738 |
const deleteBtn = e.target.closest('.delete-library-item');
|
2739 |
+
const favoriteBtn = e.target.closest('.toggle-favorite');
|
2740 |
|
2741 |
if (loadBtn) {
|
2742 |
const itemId = parseInt(loadBtn.dataset.id);
|
2743 |
loadLibraryItem(itemId);
|
2744 |
+
} else if (playBtn) {
|
2745 |
+
const itemId = parseInt(playBtn.dataset.id);
|
2746 |
+
playLibraryItem(itemId);
|
2747 |
+
} else if (editBtn) {
|
2748 |
+
const itemId = parseInt(editBtn.dataset.id);
|
2749 |
+
editLibraryItem(itemId);
|
2750 |
} else if (deleteBtn) {
|
2751 |
const itemId = parseInt(deleteBtn.dataset.id);
|
2752 |
deleteLibraryItem(itemId);
|
2753 |
+
} else if (favoriteBtn) {
|
2754 |
+
const itemId = parseInt(favoriteBtn.dataset.id);
|
2755 |
+
toggleFavorite(itemId);
|
2756 |
}
|
2757 |
});
|
2758 |
}
|
2759 |
|
2760 |
+
function populateTagFilter() {
|
2761 |
+
const allTags = [...new Set(audioLibrary.flatMap(item => item.tags || []))].sort();
|
2762 |
+
tagFilter.innerHTML = '<option value="">All Tags</option>';
|
2763 |
+
allTags.forEach(tag => {
|
2764 |
+
const option = document.createElement('option');
|
2765 |
+
option.value = tag;
|
2766 |
+
option.textContent = tag;
|
2767 |
+
tagFilter.appendChild(option);
|
2768 |
+
});
|
2769 |
+
}
|
2770 |
+
|
2771 |
function loadLibraryItem(itemId) {
|
2772 |
const item = audioLibrary.find(i => i.id === itemId);
|
2773 |
if (!item) return;
|
|
|
2792 |
showStatus(`Loaded: ${item.title}`, 'fa-file-import');
|
2793 |
}
|
2794 |
|
2795 |
+
function playLibraryItem(itemId) {
|
2796 |
+
// This would play the cached audio if available
|
2797 |
+
// For now, just show a message
|
2798 |
+
showToast('Audio playback from library not implemented in demo', 'info');
|
2799 |
+
}
|
2800 |
+
|
2801 |
+
function editLibraryItem(itemId) {
|
2802 |
+
const item = audioLibrary.find(i => i.id === itemId);
|
2803 |
+
if (!item) return;
|
2804 |
+
|
2805 |
+
openNotesModal(item);
|
2806 |
+
}
|
2807 |
+
|
2808 |
function deleteLibraryItem(itemId) {
|
2809 |
if (confirm('Are you sure you want to delete this audio file?')) {
|
2810 |
audioLibrary = audioLibrary.filter(item => item.id !== itemId);
|
|
|
2814 |
}
|
2815 |
}
|
2816 |
|
2817 |
+
function toggleFavorite(itemId) {
|
2818 |
+
const item = audioLibrary.find(i => i.id === itemId);
|
2819 |
+
if (item) {
|
2820 |
+
item.isFavorite = !item.isFavorite;
|
2821 |
+
localStorage.setItem('audioLibrary', JSON.stringify(audioLibrary));
|
2822 |
+
updateLibraryView();
|
2823 |
+
showToast(item.isFavorite ? 'Added to favorites' : 'Removed from favorites', 'success', 1500);
|
2824 |
+
}
|
2825 |
+
}
|
2826 |
+
|
2827 |
function clearAudioLibrary() {
|
2828 |
audioLibrary = [];
|
2829 |
localStorage.setItem('audioLibrary', JSON.stringify([]));
|
|
|
2831 |
showToast('Audio library cleared', 'success');
|
2832 |
}
|
2833 |
|
2834 |
+
// Notes system initialization
|
2835 |
+
function initNotesSystem() {
|
2836 |
+
closeNotesModal.addEventListener('click', closeNotes);
|
2837 |
+
cancelNotes.addEventListener('click', closeNotes);
|
2838 |
+
saveNotes.addEventListener('click', saveNotesToLibrary);
|
2839 |
+
|
2840 |
+
// Tag input handling
|
2841 |
+
tagInput.addEventListener('keydown', (e) => {
|
2842 |
+
if (e.key === 'Enter' || e.key === 'Tab') {
|
2843 |
+
e.preventDefault();
|
2844 |
+
addTag(tagInput.value.trim());
|
2845 |
+
tagInput.value = '';
|
2846 |
+
}
|
2847 |
+
});
|
2848 |
+
|
2849 |
+
tagInput.addEventListener('blur', () => {
|
2850 |
+
if (tagInput.value.trim()) {
|
2851 |
+
addTag(tagInput.value.trim());
|
2852 |
+
tagInput.value = '';
|
2853 |
+
}
|
2854 |
+
});
|
2855 |
+
|
2856 |
+
// Suggested tags
|
2857 |
+
document.addEventListener('click', (e) => {
|
2858 |
+
if (e.target.classList.contains('tag-suggestion')) {
|
2859 |
+
addTag(e.target.dataset.tag);
|
2860 |
+
}
|
2861 |
+
});
|
2862 |
+
}
|
2863 |
+
|
2864 |
+
let currentEditingItem = null;
|
2865 |
+
let currentTags = [];
|
2866 |
+
|
2867 |
+
function openNotesModal(existingItem = null) {
|
2868 |
+
currentEditingItem = existingItem;
|
2869 |
+
notesModal.classList.remove('hidden');
|
2870 |
+
|
2871 |
+
if (existingItem) {
|
2872 |
+
itemTitle.value = existingItem.title;
|
2873 |
+
itemNotes.value = existingItem.notes || '';
|
2874 |
+
currentTags = existingItem.tags ? [...existingItem.tags] : [];
|
2875 |
+
} else {
|
2876 |
+
itemTitle.value = currentText.substring(0, 50) + (currentText.length > 50 ? '...' : '');
|
2877 |
+
itemNotes.value = '';
|
2878 |
+
currentTags = [];
|
2879 |
+
}
|
2880 |
+
|
2881 |
+
updateTagDisplay();
|
2882 |
+
itemTitle.focus();
|
2883 |
+
}
|
2884 |
+
|
2885 |
+
function closeNotes() {
|
2886 |
+
notesModal.classList.add('hidden');
|
2887 |
+
currentEditingItem = null;
|
2888 |
+
currentTags = [];
|
2889 |
+
}
|
2890 |
+
|
2891 |
+
function addTag(tag) {
|
2892 |
+
if (tag && !currentTags.includes(tag)) {
|
2893 |
+
currentTags.push(tag);
|
2894 |
+
updateTagDisplay();
|
2895 |
+
}
|
2896 |
+
}
|
2897 |
+
|
2898 |
+
function removeTag(tag) {
|
2899 |
+
currentTags = currentTags.filter(t => t !== tag);
|
2900 |
+
updateTagDisplay();
|
2901 |
+
}
|
2902 |
+
|
2903 |
+
function updateTagDisplay() {
|
2904 |
+
currentTags.innerHTML = '';
|
2905 |
+
currentTags.forEach(tag => {
|
2906 |
+
const tagEl = document.createElement('span');
|
2907 |
+
tagEl.className = 'tag custom';
|
2908 |
+
tagEl.innerHTML = `
|
2909 |
+
${tag}
|
2910 |
+
<button onclick="removeTag('${tag}')" class="ml-1 text-xs hover:text-red-200">
|
2911 |
+
<i class="fas fa-times" aria-hidden="true"></i>
|
2912 |
+
</button>
|
2913 |
+
`;
|
2914 |
+
currentTags.appendChild(tagEl);
|
2915 |
+
});
|
2916 |
+
}
|
2917 |
+
|
2918 |
+
function saveNotesToLibrary() {
|
2919 |
+
if (!currentAudioBlob && !currentEditingItem) {
|
2920 |
+
showToast('No audio to save', 'error');
|
2921 |
+
return;
|
2922 |
+
}
|
2923 |
+
|
2924 |
+
const title = itemTitle.value.trim() || 'Untitled Audio';
|
2925 |
+
const notes = itemNotes.value.trim();
|
2926 |
+
|
2927 |
+
if (currentEditingItem) {
|
2928 |
+
// Update existing item
|
2929 |
+
currentEditingItem.title = title;
|
2930 |
+
currentEditingItem.notes = notes;
|
2931 |
+
currentEditingItem.tags = [...currentTags];
|
2932 |
+
currentEditingItem.lastModified = new Date().toISOString();
|
2933 |
+
} else {
|
2934 |
+
// Create new item
|
2935 |
+
const libraryItem = {
|
2936 |
+
id: Date.now(),
|
2937 |
+
title: title,
|
2938 |
+
text: currentText,
|
2939 |
+
notes: notes,
|
2940 |
+
tags: [...currentTags],
|
2941 |
+
voice: selectedVoice ? selectedVoice.name : 'Unknown',
|
2942 |
+
settings: {
|
2943 |
+
rate: parseFloat(rateSelect.value),
|
2944 |
+
pitch: parseFloat(pitchSelect.value),
|
2945 |
+
model: modelSelect.value
|
2946 |
+
},
|
2947 |
+
date: new Date().toISOString(),
|
2948 |
+
size: currentAudioBlob ? currentAudioBlob.size : 0,
|
2949 |
+
duration: audioBuffer ? audioBuffer.duration : null,
|
2950 |
+
isFavorite: false
|
2951 |
+
};
|
2952 |
+
|
2953 |
+
audioLibrary.push(libraryItem);
|
2954 |
+
}
|
2955 |
+
|
2956 |
+
// Save to localStorage (without audio data for space)
|
2957 |
+
const libraryForStorage = audioLibrary.map(item => ({
|
2958 |
+
...item,
|
2959 |
+
audioData: null // Don't store large audio data
|
2960 |
+
}));
|
2961 |
+
localStorage.setItem('audioLibrary', JSON.stringify(libraryForStorage));
|
2962 |
+
|
2963 |
+
// Limit library size
|
2964 |
+
if (audioLibrary.length > 100) {
|
2965 |
+
audioLibrary = audioLibrary.slice(-100);
|
2966 |
+
}
|
2967 |
+
|
2968 |
+
updateLibraryView();
|
2969 |
+
closeNotes();
|
2970 |
+
showToast('Audio saved to library with notes', 'success');
|
2971 |
+
}
|
2972 |
+
|
2973 |
+
// Make removeTag accessible globally
|
2974 |
+
window.removeTag = removeTag;
|
2975 |
+
|
2976 |
// Keyboard shortcuts
|
2977 |
document.addEventListener('keydown', (e) => {
|
2978 |
// Ctrl+Space or Cmd+Space to play/pause
|
2979 |
if ((e.ctrlKey || e.metaKey) && e.code === 'Space') {
|
2980 |
e.preventDefault();
|
2981 |
+
playBtn.click();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2982 |
}
|
2983 |
|
2984 |
+
// Ctrl+Enter or Cmd+Enter to synthesize
|
2985 |
+
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
2986 |
+
e.preventDefault();
|
2987 |
+
synthesizeBtn.click();
|
2988 |
+
}
|
2989 |
+
|
2990 |
+
// Ctrl+S or Cmd+S to save to library
|
2991 |
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
2992 |
e.preventDefault();
|
2993 |
+
if (currentAudioBlob) {
|
2994 |
+
saveToLibraryBtn.click();
|
2995 |
}
|
2996 |
}
|
2997 |
|
|
|
2999 |
if (e.key === 'Escape') {
|
3000 |
if (!libraryModal.classList.contains('hidden')) {
|
3001 |
closeLibrary();
|
3002 |
+
} else if (!notesModal.classList.contains('hidden')) {
|
3003 |
+
closeNotes();
|
3004 |
}
|
3005 |
}
|
3006 |
});
|
|
|
3016 |
}
|
3017 |
});
|
3018 |
|
3019 |
+
notesModal.addEventListener('click', (e) => {
|
3020 |
+
if (e.target === notesModal) {
|
3021 |
+
closeNotes();
|
3022 |
+
}
|
3023 |
+
});
|
3024 |
+
|
3025 |
// Initialize the application
|
3026 |
init();
|
3027 |
});
|