Toowired commited on
Commit
eff4144
·
verified ·
1 Parent(s): fd1dd75

Update index.html

Browse files
Files changed (1) hide show
  1. 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>Enhanced 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,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">Enhanced Google TTS Document Reader</h1>
353
- <p class="text-gray-600">Natural HD English voices with BYOK (Bring Your Own Key)</p>
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
- <!-- Synthesis Progress -->
581
- <div id="synthesisProgress" class="mb-4 hidden">
582
- <div class="flex justify-between text-sm text-gray-600 mb-1">
583
- <span>Generating speech...</span>
584
- <span id="synthesisPercentage">0%</span>
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-4xl">
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
- <div class="mb-4 flex justify-between items-center">
 
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
- <div id="libraryContent" class="grid grid-cols-1 md:grid-cols-2 gap-4 max-h-96 overflow-y-auto">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 synthesisProgress = document.getElementById('synthesisProgress');
743
- const synthesisProgressBar = document.getElementById('synthesisProgressBar');
744
- const synthesisPercentage = document.getElementById('synthesisPercentage');
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
- // Enhanced synthesize speech with progress tracking
 
 
 
 
 
 
 
 
 
 
 
 
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
- // Play audio data
1473
- async function playAudioData(audioBase64) {
1474
- if (!audioContext) {
1475
- initAudioContext();
 
1476
  }
1477
 
1478
- try {
1479
- const audioData = Uint8Array.from(atob(audioBase64), c => c.charCodeAt(0));
1480
- audioBuffer = await audioContext.decodeAudioData(audioData.buffer);
1481
-
1482
- audioSource = audioContext.createBufferSource();
1483
- audioSource.buffer = audioBuffer;
1484
- audioSource.connect(audioContext.destination);
 
 
 
 
 
 
 
 
 
 
 
 
1485
 
1486
- isPlaying = true;
1487
- isPaused = false;
 
 
 
 
 
 
 
 
 
1488
  updatePlaybackButtons();
 
 
1489
 
1490
- audioSource.start(0);
1491
- startTime = audioContext.currentTime;
1492
-
1493
- startProgressTracking();
1494
- showStatus('Playing audio', 'fa-play', 'persistent');
1495
-
1496
- audioSource.onended = () => {
1497
- if (isPlaying && !isPaused) {
1498
- stopPlayback();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1499
  }
1500
- };
 
 
 
 
 
1501
  } catch (error) {
1502
- console.error('Error playing audio:', error);
1503
- showToast('Failed to play audio: ' + error.message, 'error');
1504
  }
1505
  }
1506
 
 
1507
  function startProgressTracking() {
1508
  function updateProgress() {
1509
  if (!isPlaying || isPaused || !audioSource) return;
@@ -1703,25 +2234,21 @@
1703
  }
1704
  }
1705
 
1706
- // Text file reader
1707
  function readTextFile(file) {
1708
  const reader = new FileReader();
1709
  reader.onload = (e) => {
1710
- const text = e.target.result;
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
- let pagesProcessed = 0;
1745
-
1746
- for (let i = 1; i <= numPages; i++) {
1747
- pagePromises.push(pdf.getPage(i).then(page => {
1748
- return page.getTextContent().then(textContent => {
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
- text = pagesText.join('\n\n');
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 options = {
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
- // RTF file reader
1844
- function readRTFFile(file) {
1845
  const reader = new FileReader();
1846
  reader.onload = (e) => {
1847
- let rtfText = e.target.result;
1848
-
1849
- // Basic RTF to plain text conversion
1850
- rtfText = rtfText.replace(/\\[a-z]+[0-9]*\s?/gi, '');
1851
- rtfText = rtfText.replace(/[{}]/g, '');
1852
- rtfText = rtfText.replace(/\\\\/g, '\\');
1853
- rtfText = rtfText.replace(/\\'/g, "'");
1854
- rtfText = rtfText.replace(/\\\n/g, '\n');
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
- // Markdown file reader
1872
- function readMarkdownFile(file) {
1873
  const reader = new FileReader();
1874
  reader.onload = (e) => {
1875
- let text = e.target.result;
1876
-
1877
- // Convert basic Markdown to plain text
1878
- text = text.replace(/^#{1,6}\s+/gm, ''); // Headers
1879
- text = text.replace(/\*\*(.*?)\*\*/g, '$1'); // Bold
1880
- text = text.replace(/\*(.*?)\*/g, '$1'); // Italic
1881
- text = text.replace(/_(.*?)_/g, '$1'); // Italic underscore
1882
- text = text.replace(/`(.*?)`/g, '$1'); // Inline code
1883
- text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1'); // Links
1884
- text = text.replace(/!\[([^\]]*)\]\([^)]+\)/g, ''); // Images
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
- const audioBlob = await performSynthesis();
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
- resumePlayback();
2276
- } else if (!isPlaying && currentAudioBlob) {
2277
- await startPlayback();
2278
  } else if (isPlaying) {
2279
- pausePlayback();
 
 
 
 
 
 
 
 
 
 
 
2280
  }
 
2281
  });
2282
 
2283
  stopBtn.addEventListener('click', () => {
2284
- stopPlayback();
 
 
 
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 = (!currentAudioBlob && !isSynthesizing) && !isPlaying;
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
- saveCurrentAudioToLibrary();
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 trap
2507
- const focusableElements = libraryModal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
2508
- if (focusableElements.length > 0) {
2509
- focusableElements[0].focus();
2510
- }
2511
  }
2512
 
2513
  function closeLibrary() {
@@ -2515,56 +2594,73 @@
2515
  libraryModal.setAttribute('aria-hidden', 'true');
2516
  }
2517
 
2518
- function saveCurrentAudioToLibrary() {
2519
- if (!currentAudioBlob || !currentText) {
2520
- showToast('No audio or text available to save', 'error');
2521
- return;
2522
- }
2523
 
2524
- const libraryItem = {
2525
- id: Date.now(),
2526
- title: currentText.substring(0, 50) + (currentText.length > 50 ? '...' : ''),
2527
- text: currentText,
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
- // Convert blob to base64 for storage
2539
- const reader = new FileReader();
2540
- reader.onload = () => {
2541
- libraryItem.audioData = reader.result;
2542
- audioLibrary.push(libraryItem);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2543
 
2544
- // Limit library size (keep last 50 items)
2545
- if (audioLibrary.length > 50) {
2546
- audioLibrary = audioLibrary.slice(-50);
2547
- }
2548
 
2549
- localStorage.setItem('audioLibrary', JSON.stringify(audioLibrary.map(item => ({
2550
- ...item,
2551
- audioData: null // Don't store large audio data in localStorage
2552
- }))));
2553
 
2554
- updateLibraryView();
2555
- showToast('Audio saved to library', 'success');
2556
- };
2557
- reader.readAsDataURL(currentAudioBlob);
 
 
 
2558
  }
2559
 
2560
  function updateLibraryView() {
2561
  libraryCount.textContent = `${audioLibrary.length} items`;
2562
-
2563
- if (audioLibrary.length === 0) {
 
 
 
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 saved yet</p>
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
- audioLibrary.slice().reverse().forEach(item => {
2577
  const itemEl = document.createElement('div');
2578
- itemEl.className = 'library-item';
 
 
 
 
 
2579
  itemEl.innerHTML = `
2580
- <div class="flex justify-between items-start mb-2">
2581
- <h3 class="font-medium text-gray-800 text-sm leading-tight">${item.title}</h3>
2582
- <div class="flex gap-1 ml-2">
 
 
 
 
 
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 library item">
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
- if (isPlaying) {
2671
- if (isPaused) {
2672
- resumePlayback();
2673
- } else {
2674
- pausePlayback();
2675
- }
2676
- } else if (currentAudioBlob) {
2677
- startPlayback();
2678
- }
2679
  }
2680
 
2681
- // Ctrl+S or Cmd+S to synthesize
 
 
 
 
 
 
2682
  if ((e.ctrlKey || e.metaKey) && e.key === 's') {
2683
  e.preventDefault();
2684
- if (!isSynthesizing) {
2685
- performSynthesis();
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
  });