Toowired commited on
Commit
d67da3e
·
verified ·
1 Parent(s): 2e9fe73

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +372 -40
index.html CHANGED
@@ -61,6 +61,22 @@
61
  .api-key-input:focus {
62
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
63
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  </style>
65
  </head>
66
  <body class="bg-gray-50 min-h-screen">
@@ -94,7 +110,7 @@
94
  <button id="uploadBtn" class="bg-blue-500 hover:bg-blue-600 text-white py-3 px-4 rounded-lg flex items-center justify-center transition">
95
  <i class="fas fa-upload mr-2"></i> Upload File
96
  </button>
97
- <button id="pasteBtn" class="bg-green-500 hover:bg-green-600 text-white py-3 px-4 rounded-lg flex items-center justify-center transition">
98
  <i class="fas fa-paste mr-2"></i> Paste Text
99
  </button>
100
  <div id="dropzone" class="dropzone bg-gray-50 py-3 px-4 rounded-lg flex items-center justify-center cursor-pointer">
@@ -188,10 +204,10 @@
188
  <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">
189
  <i class="fas fa-play"></i>
190
  </button>
191
- <button id="pauseBtn" class="bg-gray-300 hover:bg-gray-400 text-gray-700 rounded-full w-12 h-12 flex items-center justify-center transition">
192
  <i class="fas fa-pause"></i>
193
  </button>
194
- <button id="stopBtn" class="bg-gray-300 hover:bg-gray-400 text-gray-700 rounded-full w-12 h-12 flex items-center justify-center transition">
195
  <i class="fas fa-stop"></i>
196
  </button>
197
  </div>
@@ -202,8 +218,9 @@
202
  <span id="currentTime">0:00</span>
203
  <span id="totalTime">0:00</span>
204
  </div>
205
- <div id="progressContainer" class="w-full flex">
206
  <!-- Progress chunks will be added dynamically -->
 
207
  </div>
208
  </div>
209
  </div>
@@ -240,24 +257,25 @@
240
  const currentTime = document.getElementById('currentTime');
241
  const totalTime = document.getElementById('totalTime');
242
  const progressContainer = document.getElementById('progressContainer');
243
-
 
244
  // Audio and state variables
245
  let audioContext;
246
  let audioBuffer;
247
  let audioSource;
248
  let currentText = '';
249
  let isPlaying = false;
 
250
  let selectedVoice = null;
251
  let startTime = 0;
 
252
  let estimatedDuration = 0;
253
  let currentChunkIndex = 0;
254
  const MAX_CHUNK_SIZE = 5000; // Characters per chunk (Google TTS limit)
255
  let totalChunks = 0;
256
- let progressChunks = [];
257
  let audioQueue = [];
258
- let isPaused = false;
259
  let apiKey = localStorage.getItem('googleTTSApiKey') || '';
260
-
261
  // Initialize
262
  function init() {
263
  // Load saved API key
@@ -267,17 +285,41 @@
267
  }
268
 
269
  // Set PDF.js worker path
270
- pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.12.313/pdf.worker.min.js';
 
 
271
 
272
- // Initialize AudioContext
 
 
 
 
273
  try {
274
  audioContext = new (window.AudioContext || window.webkitAudioContext)();
275
  } catch (e) {
276
  console.error('Web Audio API not supported', e);
277
- alert('Your browser does not support the Web Audio API. Please use Chrome or Edge.');
278
  }
279
  }
280
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  // API Key Management
282
  saveKeyBtn.addEventListener('click', () => {
283
  const key = apiKeyInput.value.trim();
@@ -290,7 +332,7 @@
290
  showToast('Please enter a valid API key', 'error');
291
  }
292
  });
293
-
294
  // Load available voices from Google TTS
295
  async function loadVoices() {
296
  if (!apiKey) {
@@ -303,24 +345,30 @@
303
  `;
304
  return;
305
  }
306
-
307
  voiceGrid.innerHTML = `
308
  <div class="col-span-full text-center py-8">
309
  <div class="loading-spinner mx-auto"></div>
310
  <p class="text-gray-500 mt-2">Loading Google TTS voices...</p>
311
  </div>
312
  `;
313
-
314
  try {
315
  const languageCode = languageSelect.value;
316
  const response = await fetch(`https://texttospeech.googleapis.com/v1/voices?key=${apiKey}&languageCode=${languageCode}`);
317
 
318
  if (!response.ok) {
319
- throw new Error(`API error: ${response.status}`);
 
 
 
 
 
 
320
  }
321
 
322
  const data = await response.json();
323
- renderVoiceOptions(data.voices);
324
  } catch (error) {
325
  console.error('Error loading voices:', error);
326
  voiceGrid.innerHTML = `
@@ -328,12 +376,16 @@
328
  <i class="fas fa-exclamation-triangle text-red-400 text-2xl"></i>
329
  <p class="text-gray-500 mt-2">Failed to load voices</p>
330
  <p class="text-red-500 text-sm mt-1">${error.message}</p>
331
- <p class="text-gray-400 text-xs mt-2">Check your API key and network connection</p>
 
 
332
  </div>
333
  `;
 
 
334
  }
335
  }
336
-
337
  function renderVoiceOptions(voices) {
338
  voiceGrid.innerHTML = '';
339
 
@@ -367,10 +419,10 @@
367
  let genderIcon = 'fa-user';
368
  let genderColor = 'text-gray-500';
369
  if (voice.ssmlGender === 'FEMALE') {
370
- genderIcon = 'fa-female';
371
  genderColor = 'text-pink-500';
372
  } else if (voice.ssmlGender === 'MALE') {
373
- genderIcon = 'fa-male';
374
  genderColor = 'text-blue-500';
375
  }
376
 
@@ -427,10 +479,12 @@
427
  });
428
 
429
  // Select first voice by default
430
- const firstCard = voiceGrid.querySelector('.voice-card');
431
- if (firstCard) firstCard.click();
 
 
432
  }
433
-
434
  // Play a voice sample
435
  async function playSample(voice, text) {
436
  if (!apiKey) {
@@ -450,7 +504,82 @@
450
  showToast('Failed to play sample: ' + error.message, 'error');
451
  }
452
  }
453
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
454
  // File Handling
455
  uploadBtn.addEventListener('click', () => fileInput.click());
456
 
@@ -459,7 +588,7 @@
459
  handleFile(e.target.files[0]);
460
  }
461
  });
462
-
463
  pasteBtn.addEventListener('click', () => {
464
  documentContent.innerHTML = '<p class="text-gray-500 italic">Press Ctrl+V (Cmd+V on Mac) to paste your text...</p>';
465
  documentContent.focus();
@@ -475,33 +604,33 @@
475
 
476
  document.addEventListener('paste', pasteHandler);
477
  });
478
-
479
  // Drag and Drop
480
  ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
481
  dropzone.addEventListener(eventName, preventDefaults, false);
482
  });
483
-
484
  function preventDefaults(e) {
485
  e.preventDefault();
486
  e.stopPropagation();
487
  }
488
-
489
  ['dragenter', 'dragover'].forEach(eventName => {
490
  dropzone.addEventListener(eventName, highlight, false);
491
  });
492
-
493
  ['dragleave', 'drop'].forEach(eventName => {
494
  dropzone.addEventListener(eventName, unhighlight, false);
495
  });
496
-
497
  function highlight() {
498
  dropzone.classList.add('active');
499
  }
500
-
501
  function unhighlight() {
502
  dropzone.classList.remove('active');
503
  }
504
-
505
  dropzone.addEventListener('drop', (e) => {
506
  const dt = e.dataTransfer;
507
  const file = dt.files[0];
@@ -509,7 +638,7 @@
509
  handleFile(file);
510
  }
511
  });
512
-
513
  // File processing
514
  function handleFile(file) {
515
  const fileType = file.type;
@@ -522,12 +651,12 @@
522
  } else if (fileType === 'application/msword' ||
523
  fileType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
524
  fileName.endsWith('.doc') || fileName.endsWith('.docx')) {
525
- alert('Word document support requires conversion to text. For best results, save as plain text or PDF first.');
526
  } else {
527
- alert('Unsupported file type. Please upload a TXT or PDF file.');
528
  }
529
  }
530
-
531
  function readTextFile(file) {
532
  const reader = new FileReader();
533
  reader.onload = (e) => {
@@ -535,11 +664,11 @@
535
  };
536
  reader.readAsText(file);
537
  }
538
-
539
  function readPDFFile(file) {
540
  // Check if PDF.js is available
541
  if (typeof pdfjsLib === 'undefined') {
542
- alert('PDF.js not loaded. PDF support requires PDF.js library.');
543
  return;
544
  }
545
 
@@ -562,5 +691,208 @@
562
 
563
  Promise.all(pagePromises).then(pagesText => {
564
  text = pagesText.join('\n\n');
565
- set
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
566
  </html>
 
61
  .api-key-input:focus {
62
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
63
  }
64
+ .toast {
65
+ position: fixed;
66
+ top: 20px;
67
+ right: 20px;
68
+ padding: 12px 24px;
69
+ border-radius: 8px;
70
+ color: white;
71
+ z-index: 1000;
72
+ animation: slideIn 0.3s ease;
73
+ }
74
+ .toast.success { background-color: #10B981; }
75
+ .toast.error { background-color: #EF4444; }
76
+ @keyframes slideIn {
77
+ from { transform: translateX(100%); opacity: 0; }
78
+ to { transform: translateX(0); opacity: 1; }
79
+ }
80
  </style>
81
  </head>
82
  <body class="bg-gray-50 min-h-screen">
 
110
  <button id="uploadBtn" class="bg-blue-500 hover:bg-blue-600 text-white py-3 px-4 rounded-lg flex items-center justify-center transition">
111
  <i class="fas fa-upload mr-2"></i> Upload File
112
  </button>
113
+ <button id="pasteBtn" class="bg-green-500 hover:green-600 text-white py-3 px-4 rounded-lg flex items-center justify-center transition">
114
  <i class="fas fa-paste mr-2"></i> Paste Text
115
  </button>
116
  <div id="dropzone" class="dropzone bg-gray-50 py-3 px-4 rounded-lg flex items-center justify-center cursor-pointer">
 
204
  <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">
205
  <i class="fas fa-play"></i>
206
  </button>
207
+ <button id="pauseBtn" class="bg-gray-300 hover:bg-gray-400 text-gray-700 rounded-full w-12 h-12 flex items-center justify-center transition" disabled>
208
  <i class="fas fa-pause"></i>
209
  </button>
210
+ <button id="stopBtn" class="bg-gray-300 hover:bg-gray-400 text-gray-700 rounded-full w-12 h-12 flex items-center justify-center transition" disabled>
211
  <i class="fas fa-stop"></i>
212
  </button>
213
  </div>
 
218
  <span id="currentTime">0:00</span>
219
  <span id="totalTime">0:00</span>
220
  </div>
221
+ <div id="progressContainer" class="w-full bg-gray-200 rounded-full h-2 flex">
222
  <!-- Progress chunks will be added dynamically -->
223
+ <div id="progressBar" class="bg-blue-600 h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
224
  </div>
225
  </div>
226
  </div>
 
257
  const currentTime = document.getElementById('currentTime');
258
  const totalTime = document.getElementById('totalTime');
259
  const progressContainer = document.getElementById('progressContainer');
260
+ const progressBar = document.getElementById('progressBar');
261
+
262
  // Audio and state variables
263
  let audioContext;
264
  let audioBuffer;
265
  let audioSource;
266
  let currentText = '';
267
  let isPlaying = false;
268
+ let isPaused = false;
269
  let selectedVoice = null;
270
  let startTime = 0;
271
+ let pauseTime = 0;
272
  let estimatedDuration = 0;
273
  let currentChunkIndex = 0;
274
  const MAX_CHUNK_SIZE = 5000; // Characters per chunk (Google TTS limit)
275
  let totalChunks = 0;
 
276
  let audioQueue = [];
 
277
  let apiKey = localStorage.getItem('googleTTSApiKey') || '';
278
+
279
  // Initialize
280
  function init() {
281
  // Load saved API key
 
285
  }
286
 
287
  // Set PDF.js worker path
288
+ if (typeof pdfjsLib !== 'undefined') {
289
+ pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.12.313/pdf.worker.min.js';
290
+ }
291
 
292
+ // Initialize AudioContext on user interaction
293
+ document.addEventListener('click', initAudioContext, { once: true });
294
+ }
295
+
296
+ function initAudioContext() {
297
  try {
298
  audioContext = new (window.AudioContext || window.webkitAudioContext)();
299
  } catch (e) {
300
  console.error('Web Audio API not supported', e);
301
+ showToast('Your browser does not support the Web Audio API. Please use Chrome or Edge.', 'error');
302
  }
303
  }
304
+
305
+ // Toast notifications
306
+ function showToast(message, type = 'success', duration = 3000) {
307
+ const toast = document.createElement('div');
308
+ toast.className = `toast ${type}`;
309
+ toast.textContent = message;
310
+ document.body.appendChild(toast);
311
+
312
+ setTimeout(() => {
313
+ toast.style.animation = 'slideOut 0.3s ease';
314
+ setTimeout(() => toast.remove(), 300);
315
+ }, duration);
316
+ }
317
+
318
+ // Add slideOut animation
319
+ const style = document.createElement('style');
320
+ style.textContent = '@keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } }';
321
+ document.head.appendChild(style);
322
+
323
  // API Key Management
324
  saveKeyBtn.addEventListener('click', () => {
325
  const key = apiKeyInput.value.trim();
 
332
  showToast('Please enter a valid API key', 'error');
333
  }
334
  });
335
+
336
  // Load available voices from Google TTS
337
  async function loadVoices() {
338
  if (!apiKey) {
 
345
  `;
346
  return;
347
  }
348
+
349
  voiceGrid.innerHTML = `
350
  <div class="col-span-full text-center py-8">
351
  <div class="loading-spinner mx-auto"></div>
352
  <p class="text-gray-500 mt-2">Loading Google TTS voices...</p>
353
  </div>
354
  `;
355
+
356
  try {
357
  const languageCode = languageSelect.value;
358
  const response = await fetch(`https://texttospeech.googleapis.com/v1/voices?key=${apiKey}&languageCode=${languageCode}`);
359
 
360
  if (!response.ok) {
361
+ if (response.status === 403) {
362
+ throw new Error('Invalid API key or Text-to-Speech API not enabled');
363
+ } else if (response.status === 400) {
364
+ throw new Error('Invalid request. Check your API key.');
365
+ } else {
366
+ throw new Error(`API error: ${response.status} ${response.statusText}`);
367
+ }
368
  }
369
 
370
  const data = await response.json();
371
+ renderVoiceOptions(data.voices || []);
372
  } catch (error) {
373
  console.error('Error loading voices:', error);
374
  voiceGrid.innerHTML = `
 
376
  <i class="fas fa-exclamation-triangle text-red-400 text-2xl"></i>
377
  <p class="text-gray-500 mt-2">Failed to load voices</p>
378
  <p class="text-red-500 text-sm mt-1">${error.message}</p>
379
+ <button id="retryVoices" class="mt-2 text-blue-500 hover:text-blue-700 text-sm">
380
+ <i class="fas fa-retry mr-1"></i> Retry
381
+ </button>
382
  </div>
383
  `;
384
+
385
+ document.getElementById('retryVoices')?.addEventListener('click', loadVoices);
386
  }
387
  }
388
+
389
  function renderVoiceOptions(voices) {
390
  voiceGrid.innerHTML = '';
391
 
 
419
  let genderIcon = 'fa-user';
420
  let genderColor = 'text-gray-500';
421
  if (voice.ssmlGender === 'FEMALE') {
422
+ genderIcon = 'fa-venus';
423
  genderColor = 'text-pink-500';
424
  } else if (voice.ssmlGender === 'MALE') {
425
+ genderIcon = 'fa-mars';
426
  genderColor = 'text-blue-500';
427
  }
428
 
 
479
  });
480
 
481
  // Select first voice by default
482
+ if (naturalVoices.length > 0) {
483
+ const firstCard = voiceGrid.querySelector('.voice-card');
484
+ if (firstCard) firstCard.click();
485
+ }
486
  }
487
+
488
  // Play a voice sample
489
  async function playSample(voice, text) {
490
  if (!apiKey) {
 
504
  showToast('Failed to play sample: ' + error.message, 'error');
505
  }
506
  }
507
+
508
+ // Synthesize speech using Google TTS API
509
+ async function synthesizeSpeech(text, voice, rate = 1, pitch = 0, model = 'standard') {
510
+ if (!apiKey) {
511
+ throw new Error('API key not provided');
512
+ }
513
+
514
+ const request = {
515
+ input: { text: text },
516
+ voice: {
517
+ languageCode: voice.languageCodes[0],
518
+ name: voice.name,
519
+ ssmlGender: voice.ssmlGender
520
+ },
521
+ audioConfig: {
522
+ audioEncoding: 'MP3',
523
+ speakingRate: rate,
524
+ pitch: pitch,
525
+ effectsProfileId: model === 'studio' ? ['telephony-class-application'] : undefined
526
+ }
527
+ };
528
+
529
+ const response = await fetch(`https://texttospeech.googleapis.com/v1/text:synthesize?key=${apiKey}`, {
530
+ method: 'POST',
531
+ headers: {
532
+ 'Content-Type': 'application/json',
533
+ },
534
+ body: JSON.stringify(request)
535
+ });
536
+
537
+ if (!response.ok) {
538
+ const errorData = await response.json();
539
+ throw new Error(errorData.error?.message || `HTTP error! status: ${response.status}`);
540
+ }
541
+
542
+ const data = await response.json();
543
+ return data.audioContent;
544
+ }
545
+
546
+ // Play audio data
547
+ async function playAudioData(audioBase64) {
548
+ if (!audioContext) {
549
+ initAudioContext();
550
+ }
551
+
552
+ try {
553
+ // Decode base64 audio
554
+ const audioData = Uint8Array.from(atob(audioBase64), c => c.charCodeAt(0));
555
+ const audioBuffer = await audioContext.decodeAudioData(audioData.buffer);
556
+
557
+ // Create and play audio source
558
+ audioSource = audioContext.createBufferSource();
559
+ audioSource.buffer = audioBuffer;
560
+ audioSource.connect(audioContext.destination);
561
+
562
+ // Set up playback state
563
+ isPlaying = true;
564
+ isPaused = false;
565
+ updatePlaybackButtons();
566
+
567
+ // Start playback
568
+ audioSource.start(0);
569
+ startTime = audioContext.currentTime;
570
+
571
+ // Handle playback end
572
+ audioSource.onended = () => {
573
+ if (isPlaying && !isPaused) {
574
+ stopPlayback();
575
+ }
576
+ };
577
+ } catch (error) {
578
+ console.error('Error playing audio:', error);
579
+ showToast('Failed to play audio: ' + error.message, 'error');
580
+ }
581
+ }
582
+
583
  // File Handling
584
  uploadBtn.addEventListener('click', () => fileInput.click());
585
 
 
588
  handleFile(e.target.files[0]);
589
  }
590
  });
591
+
592
  pasteBtn.addEventListener('click', () => {
593
  documentContent.innerHTML = '<p class="text-gray-500 italic">Press Ctrl+V (Cmd+V on Mac) to paste your text...</p>';
594
  documentContent.focus();
 
604
 
605
  document.addEventListener('paste', pasteHandler);
606
  });
607
+
608
  // Drag and Drop
609
  ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
610
  dropzone.addEventListener(eventName, preventDefaults, false);
611
  });
612
+
613
  function preventDefaults(e) {
614
  e.preventDefault();
615
  e.stopPropagation();
616
  }
617
+
618
  ['dragenter', 'dragover'].forEach(eventName => {
619
  dropzone.addEventListener(eventName, highlight, false);
620
  });
621
+
622
  ['dragleave', 'drop'].forEach(eventName => {
623
  dropzone.addEventListener(eventName, unhighlight, false);
624
  });
625
+
626
  function highlight() {
627
  dropzone.classList.add('active');
628
  }
629
+
630
  function unhighlight() {
631
  dropzone.classList.remove('active');
632
  }
633
+
634
  dropzone.addEventListener('drop', (e) => {
635
  const dt = e.dataTransfer;
636
  const file = dt.files[0];
 
638
  handleFile(file);
639
  }
640
  });
641
+
642
  // File processing
643
  function handleFile(file) {
644
  const fileType = file.type;
 
651
  } else if (fileType === 'application/msword' ||
652
  fileType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
653
  fileName.endsWith('.doc') || fileName.endsWith('.docx')) {
654
+ showToast('Word document support requires conversion to text. Please save as plain text or PDF first.', 'error');
655
  } else {
656
+ showToast('Unsupported file type. Please upload a TXT or PDF file.', 'error');
657
  }
658
  }
659
+
660
  function readTextFile(file) {
661
  const reader = new FileReader();
662
  reader.onload = (e) => {
 
664
  };
665
  reader.readAsText(file);
666
  }
667
+
668
  function readPDFFile(file) {
669
  // Check if PDF.js is available
670
  if (typeof pdfjsLib === 'undefined') {
671
+ showToast('PDF.js not loaded. PDF support requires PDF.js library.', 'error');
672
  return;
673
  }
674
 
 
691
 
692
  Promise.all(pagePromises).then(pagesText => {
693
  text = pagesText.join('\n\n');
694
+ setDocumentContent(text);
695
+ }).catch(error => {
696
+ console.error('Error extracting PDF text:', error);
697
+ showToast('Failed to extract text from PDF', 'error');
698
+ });
699
+ }).catch(error => {
700
+ console.error('Error loading PDF:', error);
701
+ showToast('Failed to load PDF file', 'error');
702
+ });
703
+ }
704
+
705
+ // Set document content
706
+ function setDocumentContent(text) {
707
+ currentText = text;
708
+ documentContent.innerHTML = text.split('\n').map(line => `<p>${line || '&nbsp;'}</p>`).join('');
709
+ charCount.textContent = `${text.length} characters`;
710
+
711
+ if (text.length > 1000000) {
712
+ showToast('Document exceeds 1,000,000 character limit. Some content may be truncated.', 'error');
713
+ }
714
+ }
715
+
716
+ // Playback controls
717
+ playBtn.addEventListener('click', async () => {
718
+ if (!selectedVoice) {
719
+ showToast('Please select a voice first', 'error');
720
+ return;
721
+ }
722
+
723
+ if (!currentText.trim()) {
724
+ showToast('Please add some text to read', 'error');
725
+ return;
726
+ }
727
+
728
+ if (isPaused) {
729
+ // Resume playback
730
+ resumePlayback();
731
+ } else {
732
+ // Start new playback
733
+ await startPlayback();
734
+ }
735
+ });
736
+
737
+ pauseBtn.addEventListener('click', () => {
738
+ if (isPlaying && !isPaused) {
739
+ pausePlayback();
740
+ }
741
+ });
742
+
743
+ stopBtn.addEventListener('click', () => {
744
+ stopPlayback();
745
+ });
746
+
747
+ async function startPlayback() {
748
+ try {
749
+ isPlaying = true;
750
+ isPaused = false;
751
+ updatePlaybackButtons();
752
+
753
+ showToast('Generating speech...', 'success', 1000);
754
+
755
+ // Split text into chunks if needed
756
+ const chunks = splitTextIntoChunks(currentText, MAX_CHUNK_SIZE);
757
+ totalChunks = chunks.length;
758
+
759
+ if (chunks.length > 1) {
760
+ showToast(`Processing ${chunks.length} chunks...`, 'success', 2000);
761
+ }
762
+
763
+ // Process all chunks
764
+ for (let i = 0; i < chunks.length; i++) {
765
+ if (!isPlaying || isPaused) break;
766
+
767
+ currentChunkIndex = i;
768
+ const audioData = await synthesizeSpeech(
769
+ chunks[i],
770
+ selectedVoice,
771
+ parseFloat(rateSelect.value),
772
+ parseFloat(pitchSelect.value),
773
+ modelSelect.value
774
+ );
775
+
776
+ if (i === 0) {
777
+ // Start playing first chunk immediately
778
+ await playAudioData(audioData);
779
+ } else {
780
+ // Queue other chunks
781
+ audioQueue.push(audioData);
782
+ }
783
+ }
784
+
785
+ } catch (error) {
786
+ console.error('Error during playback:', error);
787
+ showToast('Failed to play text: ' + error.message, 'error');
788
+ stopPlayback();
789
+ }
790
+ }
791
+
792
+ function pausePlayback() {
793
+ if (audioSource && isPlaying && !isPaused) {
794
+ audioSource.stop();
795
+ pauseTime = audioContext.currentTime - startTime;
796
+ isPaused = true;
797
+ updatePlaybackButtons();
798
+ }
799
+ }
800
+
801
+ function resumePlayback() {
802
+ if (isPaused && audioBuffer) {
803
+ // Create new source for resume
804
+ audioSource = audioContext.createBufferSource();
805
+ audioSource.buffer = audioBuffer;
806
+ audioSource.connect(audioContext.destination);
807
+
808
+ // Resume from pause point
809
+ audioSource.start(0, pauseTime);
810
+ startTime = audioContext.currentTime - pauseTime;
811
+ isPaused = false;
812
+ updatePlaybackButtons();
813
+
814
+ audioSource.onended = () => {
815
+ if (isPlaying && !isPaused) {
816
+ stopPlayback();
817
+ }
818
+ };
819
+ }
820
+ }
821
+
822
+ function stopPlayback() {
823
+ if (audioSource && isPlaying) {
824
+ audioSource.stop();
825
+ }
826
+
827
+ isPlaying = false;
828
+ isPaused = false;
829
+ startTime = 0;
830
+ pauseTime = 0;
831
+ currentChunkIndex = 0;
832
+ audioQueue = [];
833
+ updatePlaybackButtons();
834
+ updateProgress(0);
835
+ }
836
+
837
+ function updatePlaybackButtons() {
838
+ playBtn.disabled = isPlaying && !isPaused;
839
+ pauseBtn.disabled = !isPlaying || isPaused;
840
+ stopBtn.disabled = !isPlaying;
841
+
842
+ if (isPlaying && !isPaused) {
843
+ playBtn.innerHTML = '<i class="fas fa-pause"></i>';
844
+ } else {
845
+ playBtn.innerHTML = '<i class="fas fa-play"></i>';
846
+ }
847
+ }
848
+
849
+ function updateProgress(progress) {
850
+ progressBar.style.width = (progress * 100) + '%';
851
+
852
+ const currentSeconds = Math.floor(progress * estimatedDuration);
853
+ const totalSeconds = Math.floor(estimatedDuration);
854
+
855
+ currentTime.textContent = formatTime(currentSeconds);
856
+ totalTime.textContent = formatTime(totalSeconds);
857
+ }
858
+
859
+ function formatTime(seconds) {
860
+ const minutes = Math.floor(seconds / 60);
861
+ const secs = seconds % 60;
862
+ return `${minutes}:${secs.toString().padStart(2, '0')}`;
863
+ }
864
+
865
+ function splitTextIntoChunks(text, maxChunkSize) {
866
+ const chunks = [];
867
+ const sentences = text.split(/[.!?]+/).filter(s => s.trim());
868
+
869
+ let currentChunk = '';
870
+
871
+ for (const sentence of sentences) {
872
+ if (currentChunk.length + sentence.length <= maxChunkSize) {
873
+ currentChunk += sentence + '.';
874
+ } else {
875
+ if (currentChunk) {
876
+ chunks.push(currentChunk.trim());
877
+ }
878
+ currentChunk = sentence + '.';
879
+ }
880
+ }
881
+
882
+ if (currentChunk) {
883
+ chunks.push(currentChunk.trim());
884
+ }
885
+
886
+ return chunks;
887
+ }
888
+
889
+ // Event listeners
890
+ refreshVoicesBtn.addEventListener('click', loadVoices);
891
+ languageSelect.addEventListener('change', loadVoices);
892
+
893
+ // Initialize the application
894
+ init();
895
+ });
896
+ </script>
897
+ </body>
898
  </html>