Toowired commited on
Commit
c1ea3c1
·
verified ·
1 Parent(s): f570f36

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +356 -153
index.html CHANGED
@@ -117,6 +117,67 @@
117
  padding: 0 2px;
118
  transition: all 0.2s;
119
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  </style>
121
  </head>
122
  <body class="bg-gray-50 min-h-screen">
@@ -213,10 +274,10 @@
213
  </div>
214
  </div>
215
 
216
- <!-- Voice Selection -->
217
  <div class="border-t border-gray-200 p-6 bg-gray-50">
218
  <div class="flex justify-between items-center mb-4">
219
- <h2 class="text-xl font-semibold text-gray-800">Select Google TTS Voice</h2>
220
  <div class="flex items-center gap-2">
221
  <div class="relative">
222
  <select id="languageSelect" class="bg-white border border-gray-300 rounded-md py-1 px-3 pr-8 text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">
@@ -258,84 +319,121 @@
258
  </div>
259
  </div>
260
 
261
- <!-- Voice Selection Dropdown -->
262
- <div id="voiceSelectionContainer" class="mb-4">
263
- <!-- Voice dropdown will be inserted here -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  </div>
265
-
266
- <div id="voiceGrid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
267
- <!-- Voices will be loaded dynamically -->
268
- <div class="text-center py-8">
269
- <div class="loading-spinner mx-auto"></div>
270
- <p class="text-gray-500 mt-2">Loading Google TTS voices...</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
  </div>
272
  </div>
273
  </div>
274
 
275
- <!-- Voice Controls -->
276
- <div class="bg-gray-100 p-6">
277
- <div class="controls flex flex-wrap justify-between items-center gap-4 mb-4">
278
- <div class="voice-options flex flex-wrap items-center gap-4">
279
- <div>
280
- <label for="rateSelect" class="block text-sm font-medium text-gray-700 mb-1">Speed</label>
281
- <select id="rateSelect" class="bg-white border border-gray-300 rounded-md py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500">
282
- <option value="0.5">0.5x</option>
283
- <option value="0.8">0.8x</option>
284
- <option value="1" selected>1x</option>
285
- <option value="1.2">1.2x</option>
286
- <option value="1.5">1.5x</option>
287
- <option value="2">2x</option>
288
- </select>
289
- </div>
290
-
291
- <div>
292
- <label for="pitchSelect" class="block text-sm font-medium text-gray-700 mb-1">Pitch</label>
293
- <select id="pitchSelect" class="bg-white border border-gray-300 rounded-md py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500">
294
- <option value="-20">Low</option>
295
- <option value="0" selected>Normal</option>
296
- <option value="20">High</option>
297
- </select>
298
- </div>
299
-
300
- <div>
301
- <label for="modelSelect" class="block text-sm font-medium text-gray-700 mb-1">Voice Model</label>
302
- <select id="modelSelect" class="bg-white border border-gray-300 rounded-md py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500">
303
- <option value="studio">Studio Quality - $160.00/1M chars</option>
304
- <option value="wavenet" selected>WaveNet - $16.00/1M chars</option>
305
- <option value="neural2">Neural2 - $16.00/1M chars</option>
306
- <option value="standard">Standard - $4.00/1M chars</option>
307
- </select>
308
- </div>
309
  </div>
310
 
311
- <div class="playback-controls flex items-center gap-2">
312
- <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">
313
- <i class="fas fa-play"></i>
314
  </button>
315
- <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>
316
  <i class="fas fa-pause"></i>
317
  </button>
318
- <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>
319
  <i class="fas fa-stop"></i>
320
  </button>
321
- <button id="downloadBtn" class="bg-green-600 hover:bg-green-700 text-white rounded-full w-12 h-12 flex items-center justify-center transition" disabled title="Download Audio">
322
  <i class="fas fa-download"></i>
323
  </button>
324
  </div>
325
  </div>
326
 
 
327
  <div class="progress-container">
328
- <div class="flex justify-between text-sm text-gray-600 mb-1">
329
  <span id="currentTime">0:00</span>
 
 
 
330
  <span id="totalTime">0:00</span>
331
  </div>
332
- <div id="progressContainer" class="w-full bg-gray-200 rounded-full h-4 flex relative cursor-pointer">
333
  <!-- Progress bar for audio -->
334
- <div id="progressBar" class="bg-blue-600 h-4 rounded-full transition-all duration-300" style="width: 0%"></div>
335
  <!-- Reading progress indicator -->
336
- <div id="readingProgress" class="absolute inset-0 bg-green-500 bg-opacity-30 h-4 rounded-full transition-all duration-300" style="width: 0%"></div>
337
  <!-- Current position marker -->
338
- <div id="currentPositionMarker" class="absolute top-0 w-1 h-4 bg-red-500 transition-all duration-100" style="left: 0%"></div>
339
  </div>
340
  </div>
341
  </div>
@@ -363,6 +461,7 @@
363
  let currentAudioBlob = null;
364
  let currentReadingPosition = 0;
365
  let estimatedDuration = 0;
 
366
 
367
  const MAX_CHUNK_SIZE = 5000;
368
  const PRICING = {
@@ -371,6 +470,18 @@
371
  neural2: 16.00,
372
  studio: 160.00
373
  };
 
 
 
 
 
 
 
 
 
 
 
 
374
 
375
  // Global error handler
376
  window.addEventListener('error', (e) => {
@@ -392,8 +503,9 @@
392
  const fileInput = document.getElementById('fileInput');
393
  const documentContent = document.getElementById('documentContent');
394
  const charCount = document.getElementById('charCount');
395
- const voiceGrid = document.getElementById('voiceGrid');
396
- const voiceSelectionContainer = document.getElementById('voiceSelectionContainer');
 
397
  const refreshVoicesBtn = document.getElementById('refreshVoicesBtn');
398
  const languageSelect = document.getElementById('languageSelect');
399
  const rateSelect = document.getElementById('rateSelect');
@@ -403,12 +515,15 @@
403
  const pauseBtn = document.getElementById('pauseBtn');
404
  const stopBtn = document.getElementById('stopBtn');
405
  const downloadBtn = document.getElementById('downloadBtn');
 
406
  const currentTime = document.getElementById('currentTime');
407
  const totalTime = document.getElementById('totalTime');
408
  const progressContainer = document.getElementById('progressContainer');
409
  const progressBar = document.getElementById('progressBar');
410
  const readingProgress = document.getElementById('readingProgress');
411
  const currentPositionMarker = document.getElementById('currentPositionMarker');
 
 
412
 
413
  // Dark mode elements
414
  const darkModeToggle = document.getElementById('darkModeToggle');
@@ -528,6 +643,34 @@
528
  }
529
  }
530
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
531
  // Auto select voice for model
532
  function autoSelectVoiceForModel(model) {
533
  if (!availableVoices.length) return;
@@ -549,13 +692,11 @@
549
  }
550
 
551
  if (preferredVoices.length > 0) {
552
- selectedVoice = preferredVoices[0];
553
- const voiceSelect = document.getElementById('voiceSelect');
554
- if (voiceSelect) {
555
- voiceSelect.value = selectedVoice.name;
556
- }
557
- updateCostEstimator();
558
- showToast(`Voice changed to ${selectedVoice.name} for ${model} model`, 'success', 2000);
559
  }
560
  }
561
 
@@ -723,7 +864,12 @@
723
  const url = URL.createObjectURL(currentAudioBlob);
724
  const a = document.createElement('a');
725
  a.href = url;
726
- a.download = `tts-audio-${new Date().toISOString().split('T')[0]}.mp3`;
 
 
 
 
 
727
  document.body.appendChild(a);
728
  a.click();
729
  document.body.removeChild(a);
@@ -738,6 +884,9 @@
738
 
739
  // Toast notifications
740
  function showToast(message, type = 'success', duration = 3000) {
 
 
 
741
  const toast = document.createElement('div');
742
  toast.className = `toast ${type}`;
743
  toast.textContent = message;
@@ -810,22 +959,11 @@
810
  // Load available voices from Google TTS
811
  async function loadVoices() {
812
  if (!apiKey) {
813
- voiceGrid.innerHTML = `
814
- <div class="col-span-full text-center py-8">
815
- <i class="fas fa-key text-gray-400 text-2xl"></i>
816
- <p class="text-gray-500 mt-2">Please enter your Google Cloud API key</p>
817
- <p class="text-gray-400 text-sm mt-1">The key must have Text-to-Speech API enabled</p>
818
- </div>
819
- `;
820
  return;
821
  }
822
 
823
- voiceGrid.innerHTML = `
824
- <div class="col-span-full text-center py-8">
825
- <div class="loading-spinner mx-auto"></div>
826
- <p class="text-gray-500 mt-2">Loading Google TTS voices...</p>
827
- </div>
828
- `;
829
 
830
  try {
831
  const languageCode = languageSelect.value;
@@ -860,96 +998,152 @@
860
  availableVoices = naturalVoices;
861
 
862
  if (naturalVoices.length === 0) {
863
- voiceGrid.innerHTML = `
864
- <div class="col-span-full text-center py-8">
865
- <i class="fas fa-microphone-slash text-gray-400 text-2xl"></i>
866
- <p class="text-gray-500 mt-2">No high-quality voices available for selected language</p>
867
- </div>
868
- `;
869
  return;
870
  }
871
 
872
- // Create voice dropdown selector
873
- createVoiceDropdown(naturalVoices);
874
 
875
- // Show success message
876
- voiceGrid.innerHTML = `
877
- <div class="col-span-full text-center py-8">
878
- <i class="fas fa-check-circle text-green-500 text-2xl"></i>
879
- <p class="text-gray-500 mt-2">${naturalVoices.length} voices loaded successfully</p>
880
- <p class="text-gray-400 text-sm mt-1">Voice can be selected from the dropdown above</p>
881
- </div>
882
- `;
883
-
884
- // Automatically select the first available voice based on current model
885
- const currentModel = modelSelect.value || 'wavenet';
886
- autoSelectVoiceForModel(currentModel);
887
  updateCostEstimator();
888
 
 
 
889
  } catch (error) {
890
  console.error('Error loading voices:', error);
891
-
892
- voiceGrid.innerHTML = `
893
- <div class="col-span-full text-center py-8">
894
- <i class="fas fa-exclamation-triangle text-red-400 text-2xl"></i>
895
- <p class="text-gray-500 mt-2">Failed to load voices</p>
896
- <p class="text-red-500 text-sm mt-1 max-w-md mx-auto">${error.message}</p>
897
- <button id="retryVoices" class="mt-4 bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded text-sm">
898
- <i class="fas fa-sync-alt mr-1"></i> Retry
899
- </button>
 
900
  </div>
901
  `;
902
-
903
- document.getElementById('retryVoices')?.addEventListener('click', loadVoices);
904
- }
905
  }
906
 
907
- // Create voice dropdown selector
908
- function createVoiceDropdown(voices) {
909
- // Clear existing dropdown if any
910
- voiceSelectionContainer.innerHTML = '';
911
-
912
- // Create voice selector dropdown
913
- const voiceDropdownContainer = document.createElement('div');
914
- voiceDropdownContainer.className = 'mb-4';
915
- voiceDropdownContainer.innerHTML = `
916
- <label for="voiceSelect" class="block text-sm font-medium text-gray-700 mb-1">Voice Selection</label>
917
- <select id="voiceSelect" class="bg-white border border-gray-300 rounded-md py-2 px-3 w-full focus:outline-none focus:ring-blue-500 focus:border-blue-500">
918
- <option value="">Select a voice...</option>
919
- </select>
920
- `;
921
-
922
- voiceSelectionContainer.appendChild(voiceDropdownContainer);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
923
 
924
- const voiceSelect = document.getElementById('voiceSelect');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
925
 
926
- // Populate dropdown with voices
927
- voices.forEach(voice => {
928
- const option = document.createElement('option');
929
- option.value = voice.name;
930
- option.textContent = `${voice.name} (${voice.ssmlGender})`;
931
- voiceSelect.appendChild(option);
932
  });
 
 
 
 
933
 
934
- // Add event listener for voice selection
935
- voiceSelect.addEventListener('change', (e) => {
936
- const selectedVoiceName = e.target.value;
937
- if (selectedVoiceName) {
938
- selectedVoice = voices.find(voice => voice.name === selectedVoiceName);
939
- updateCostEstimator();
940
- showToast(`Voice changed to ${selectedVoice.name}`, 'success', 1500);
941
- }
942
- });
 
 
 
 
 
 
 
 
 
 
943
 
944
- // Auto-select first voice
945
- if (voices.length > 0) {
946
- voiceSelect.value = voices[0].name;
947
- selectedVoice = voices[0];
948
- updateCostEstimator();
949
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
950
  }
951
 
952
- // Synthesize speech
953
  async function synthesizeSpeech(text, voice, rate = 1, pitch = 0, model = 'standard') {
954
  if (!apiKey) {
955
  throw new Error('API key not provided');
@@ -1582,7 +1776,7 @@
1582
  // Playback controls
1583
  playBtn.addEventListener('click', async () => {
1584
  if (!selectedVoice) {
1585
- showToast('Please select a voice from the dropdown', 'error');
1586
  return;
1587
  }
1588
 
@@ -1608,7 +1802,7 @@
1608
  stopPlayback();
1609
  });
1610
 
1611
- // Playback functions
1612
  async function startPlayback() {
1613
  try {
1614
  isPlaying = true;
@@ -1625,9 +1819,15 @@
1625
 
1626
  let allAudioData = [];
1627
 
 
 
 
1628
  for (let i = 0; i < chunks.length; i++) {
1629
  if (!isPlaying || isPaused) break;
1630
 
 
 
 
1631
  const audioData = await synthesizeSpeech(
1632
  chunks[i],
1633
  selectedVoice,
@@ -1639,11 +1839,14 @@
1639
  allAudioData.push(audioData);
1640
  }
1641
 
 
 
1642
  if (allAudioData.length > 0 && isPlaying) {
1643
  await combineAndPlayAudio(allAudioData);
1644
  }
1645
 
1646
  } catch (error) {
 
1647
  console.error('Error during playback:', error);
1648
  showToast('Failed to play text: ' + error.message, 'error');
1649
  stopPlayback();
@@ -1730,9 +1933,9 @@
1730
  downloadBtn.disabled = !currentAudioBlob;
1731
 
1732
  if (isPlaying && !isPaused) {
1733
- playBtn.innerHTML = '<i class="fas fa-pause"></i>';
1734
  } else {
1735
- playBtn.innerHTML = '<i class="fas fa-play"></i>';
1736
  }
1737
  }
1738
 
 
117
  padding: 0 2px;
118
  transition: all 0.2s;
119
  }
120
+
121
+ /* Enhanced voice selector styles */
122
+ .voice-selector {
123
+ background: linear-gradient(145deg, #f9fafb, #ffffff);
124
+ border-radius: 12px;
125
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
126
+ }
127
+
128
+ .voice-option {
129
+ transition: all 0.2s ease;
130
+ border-radius: 8px;
131
+ }
132
+
133
+ .voice-option:hover {
134
+ background-color: #f3f4f6;
135
+ transform: translateY(-1px);
136
+ }
137
+
138
+ .voice-option.selected {
139
+ background-color: #3b82f6;
140
+ color: white;
141
+ }
142
+
143
+ .voice-category {
144
+ border-left: 4px solid #3b82f6;
145
+ background-color: #f8fafc;
146
+ }
147
+
148
+ /* Audio controls enhancement */
149
+ .audio-controls {
150
+ background: linear-gradient(145deg, #ffffff, #f9fafb);
151
+ border-radius: 16px;
152
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
153
+ }
154
+
155
+ .control-button {
156
+ transition: all 0.2s ease;
157
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
158
+ }
159
+
160
+ .control-button:hover {
161
+ transform: translateY(-2px);
162
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
163
+ }
164
+
165
+ .control-button:active {
166
+ transform: translateY(0);
167
+ }
168
+
169
+ /* Enhanced progress bar */
170
+ .progress-bar-container {
171
+ background: linear-gradient(145deg, #e5e7eb, #f3f4f6);
172
+ border-radius: 12px;
173
+ overflow: hidden;
174
+ box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
175
+ }
176
+
177
+ .progress-bar {
178
+ background: linear-gradient(45deg, #3b82f6, #6366f1);
179
+ box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3);
180
+ }
181
  </style>
182
  </head>
183
  <body class="bg-gray-50 min-h-screen">
 
274
  </div>
275
  </div>
276
 
277
+ <!-- Enhanced Voice Selection -->
278
  <div class="border-t border-gray-200 p-6 bg-gray-50">
279
  <div class="flex justify-between items-center mb-4">
280
+ <h2 class="text-xl font-semibold text-gray-800">Voice & Settings</h2>
281
  <div class="flex items-center gap-2">
282
  <div class="relative">
283
  <select id="languageSelect" class="bg-white border border-gray-300 rounded-md py-1 px-3 pr-8 text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">
 
319
  </div>
320
  </div>
321
 
322
+ <!-- Enhanced Voice Selector -->
323
+ <div class="voice-selector p-4 mb-6">
324
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
325
+ <!-- Male Voices -->
326
+ <div class="voice-category p-3 rounded-lg">
327
+ <h3 class="font-semibold text-gray-700 mb-2 flex items-center">
328
+ <i class="fas fa-mars text-blue-500 mr-2"></i>
329
+ Male Voices
330
+ </h3>
331
+ <div id="maleVoices" class="space-y-2">
332
+ <!-- Male voices will be populated here -->
333
+ </div>
334
+ </div>
335
+
336
+ <!-- Female Voices -->
337
+ <div class="voice-category p-3 rounded-lg">
338
+ <h3 class="font-semibold text-gray-700 mb-2 flex items-center">
339
+ <i class="fas fa-venus text-pink-500 mr-2"></i>
340
+ Female Voices
341
+ </h3>
342
+ <div id="femaleVoices" class="space-y-2">
343
+ <!-- Female voices will be populated here -->
344
+ </div>
345
+ </div>
346
+
347
+ <!-- Neutral Voices -->
348
+ <div class="voice-category p-3 rounded-lg">
349
+ <h3 class="font-semibold text-gray-700 mb-2 flex items-center">
350
+ <i class="fas fa-user text-purple-500 mr-2"></i>
351
+ Neutral Voices
352
+ </h3>
353
+ <div id="neutralVoices" class="space-y-2">
354
+ <!-- Neutral voices will be populated here -->
355
+ </div>
356
+ </div>
357
+ </div>
358
  </div>
359
+
360
+ <!-- Voice Settings Grid -->
361
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
362
+ <div>
363
+ <label for="rateSelect" class="block text-sm font-medium text-gray-700 mb-1">Speed</label>
364
+ <select id="rateSelect" class="bg-white border border-gray-300 rounded-md py-2 px-3 w-full focus:outline-none focus:ring-blue-500 focus:border-blue-500">
365
+ <option value="0.5">0.5x (Slow)</option>
366
+ <option value="0.8">0.8x</option>
367
+ <option value="1" selected>1x (Normal)</option>
368
+ <option value="1.2">1.2x</option>
369
+ <option value="1.5">1.5x (Fast)</option>
370
+ <option value="2">2x (Very Fast)</option>
371
+ </select>
372
+ </div>
373
+
374
+ <div>
375
+ <label for="pitchSelect" class="block text-sm font-medium text-gray-700 mb-1">Pitch</label>
376
+ <select id="pitchSelect" class="bg-white border border-gray-300 rounded-md py-2 px-3 w-full focus:outline-none focus:ring-blue-500 focus:border-blue-500">
377
+ <option value="-20">Low (-20st)</option>
378
+ <option value="-10">Lower (-10st)</option>
379
+ <option value="0" selected>Normal (0st)</option>
380
+ <option value="10">Higher (+10st)</option>
381
+ <option value="20">High (+20st)</option>
382
+ </select>
383
+ </div>
384
+
385
+ <div>
386
+ <label for="modelSelect" class="block text-sm font-medium text-gray-700 mb-1">Voice Model</label>
387
+ <select id="modelSelect" class="bg-white border border-gray-300 rounded-md py-2 px-3 w-full focus:outline-none focus:ring-blue-500 focus:border-blue-500">
388
+ <option value="wavenet" selected>WaveNet - $16.00/1M</option>
389
+ <option value="neural2">Neural2 - $16.00/1M</option>
390
+ <option value="studio">Studio - $160.00/1M</option>
391
+ <option value="standard">Standard - $4.00/1M</option>
392
+ </select>
393
  </div>
394
  </div>
395
  </div>
396
 
397
+ <!-- Enhanced Audio Controls -->
398
+ <div class="audio-controls p-6 m-4">
399
+ <div class="flex flex-wrap justify-between items-center gap-4 mb-4">
400
+ <div class="flex items-center gap-2">
401
+ <span class="text-sm font-medium text-gray-700">Selected Voice:</span>
402
+ <span id="selectedVoiceName" class="text-sm text-blue-600 font-medium">None</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
403
  </div>
404
 
405
+ <div class="playback-controls flex items-center gap-3">
406
+ <button id="playBtn" class="control-button bg-blue-600 hover:bg-blue-700 text-white rounded-full w-14 h-14 flex items-center justify-center transition">
407
+ <i class="fas fa-play text-lg"></i>
408
  </button>
409
+ <button id="pauseBtn" class="control-button bg-gray-300 hover:bg-gray-400 text-gray-700 rounded-full w-12 h-12 flex items-center justify-center transition" disabled>
410
  <i class="fas fa-pause"></i>
411
  </button>
412
+ <button id="stopBtn" class="control-button bg-gray-300 hover:bg-gray-400 text-gray-700 rounded-full w-12 h-12 flex items-center justify-center transition" disabled>
413
  <i class="fas fa-stop"></i>
414
  </button>
415
+ <button id="downloadBtn" class="control-button bg-green-600 hover:bg-green-700 text-white rounded-full w-12 h-12 flex items-center justify-center transition" disabled title="Download Audio">
416
  <i class="fas fa-download"></i>
417
  </button>
418
  </div>
419
  </div>
420
 
421
+ <!-- Enhanced Progress Container -->
422
  <div class="progress-container">
423
+ <div class="flex justify-between text-sm text-gray-600 mb-2">
424
  <span id="currentTime">0:00</span>
425
+ <span class="text-xs text-gray-500">
426
+ <span id="synthesisProgress" class="hidden">Synthesizing... <span id="progressPercentage">0%</span></span>
427
+ </span>
428
  <span id="totalTime">0:00</span>
429
  </div>
430
+ <div id="progressContainer" class="progress-bar-container w-full h-4 flex relative cursor-pointer">
431
  <!-- Progress bar for audio -->
432
+ <div id="progressBar" class="progress-bar h-full rounded-full transition-all duration-300" style="width: 0%"></div>
433
  <!-- Reading progress indicator -->
434
+ <div id="readingProgress" class="absolute inset-0 bg-green-500 bg-opacity-30 h-full rounded-full transition-all duration-300" style="width: 0%"></div>
435
  <!-- Current position marker -->
436
+ <div id="currentPositionMarker" class="absolute top-0 w-1 h-full bg-red-500 transition-all duration-100" style="left: 0%"></div>
437
  </div>
438
  </div>
439
  </div>
 
461
  let currentAudioBlob = null;
462
  let currentReadingPosition = 0;
463
  let estimatedDuration = 0;
464
+ let synthesisProgressInterval = null;
465
 
466
  const MAX_CHUNK_SIZE = 5000;
467
  const PRICING = {
 
470
  neural2: 16.00,
471
  studio: 160.00
472
  };
473
+
474
+ // Default male voice preference
475
+ const defaultMaleVoices = [
476
+ 'en-US-Wavenet-A',
477
+ 'en-US-Wavenet-B',
478
+ 'en-US-Wavenet-D',
479
+ 'en-US-Neural2-A',
480
+ 'en-US-Neural2-D',
481
+ 'en-US-Standard-A',
482
+ 'en-US-Standard-B',
483
+ 'en-US-Standard-D'
484
+ ];
485
 
486
  // Global error handler
487
  window.addEventListener('error', (e) => {
 
503
  const fileInput = document.getElementById('fileInput');
504
  const documentContent = document.getElementById('documentContent');
505
  const charCount = document.getElementById('charCount');
506
+ const maleVoices = document.getElementById('maleVoices');
507
+ const femaleVoices = document.getElementById('femaleVoices');
508
+ const neutralVoices = document.getElementById('neutralVoices');
509
  const refreshVoicesBtn = document.getElementById('refreshVoicesBtn');
510
  const languageSelect = document.getElementById('languageSelect');
511
  const rateSelect = document.getElementById('rateSelect');
 
515
  const pauseBtn = document.getElementById('pauseBtn');
516
  const stopBtn = document.getElementById('stopBtn');
517
  const downloadBtn = document.getElementById('downloadBtn');
518
+ const selectedVoiceName = document.getElementById('selectedVoiceName');
519
  const currentTime = document.getElementById('currentTime');
520
  const totalTime = document.getElementById('totalTime');
521
  const progressContainer = document.getElementById('progressContainer');
522
  const progressBar = document.getElementById('progressBar');
523
  const readingProgress = document.getElementById('readingProgress');
524
  const currentPositionMarker = document.getElementById('currentPositionMarker');
525
+ const synthesisProgress = document.getElementById('synthesisProgress');
526
+ const progressPercentage = document.getElementById('progressPercentage');
527
 
528
  // Dark mode elements
529
  const darkModeToggle = document.getElementById('darkModeToggle');
 
643
  }
644
  }
645
 
646
+ // Find and select default male voice
647
+ function selectDefaultMaleVoice() {
648
+ if (!availableVoices.length) return;
649
+
650
+ for (const preferredVoice of defaultMaleVoices) {
651
+ const voice = availableVoices.find(v => v.name === preferredVoice);
652
+ if (voice) {
653
+ selectVoice(voice);
654
+ return voice;
655
+ }
656
+ }
657
+
658
+ // Fallback to first male voice if none of the preferred ones are found
659
+ const maleVoice = availableVoices.find(v => v.ssmlGender === 'MALE');
660
+ if (maleVoice) {
661
+ selectVoice(maleVoice);
662
+ return maleVoice;
663
+ }
664
+
665
+ // Final fallback to any voice
666
+ if (availableVoices.length > 0) {
667
+ selectVoice(availableVoices[0]);
668
+ return availableVoices[0];
669
+ }
670
+
671
+ return null;
672
+ }
673
+
674
  // Auto select voice for model
675
  function autoSelectVoiceForModel(model) {
676
  if (!availableVoices.length) return;
 
692
  }
693
 
694
  if (preferredVoices.length > 0) {
695
+ // Prefer male voices if available
696
+ const maleVoice = preferredVoices.find(v => v.ssmlGender === 'MALE');
697
+ const voiceToSelect = maleVoice || preferredVoices[0];
698
+ selectVoice(voiceToSelect);
699
+ showToast(`Voice changed to ${voiceToSelect.name} for ${model} model`, 'success', 2000);
 
 
700
  }
701
  }
702
 
 
864
  const url = URL.createObjectURL(currentAudioBlob);
865
  const a = document.createElement('a');
866
  a.href = url;
867
+
868
+ // Create a more descriptive filename
869
+ const timestamp = new Date().toISOString().slice(0, -5).replace(/[T:]/g, '-');
870
+ const voiceName = selectedVoice ? selectedVoice.name.replace(/[^a-zA-Z0-9]/g, '-') : 'unknown';
871
+ a.download = `tts-${voiceName}-${timestamp}.mp3`;
872
+
873
  document.body.appendChild(a);
874
  a.click();
875
  document.body.removeChild(a);
 
884
 
885
  // Toast notifications
886
  function showToast(message, type = 'success', duration = 3000) {
887
+ // Remove existing toasts
888
+ document.querySelectorAll('.toast').forEach(toast => toast.remove());
889
+
890
  const toast = document.createElement('div');
891
  toast.className = `toast ${type}`;
892
  toast.textContent = message;
 
959
  // Load available voices from Google TTS
960
  async function loadVoices() {
961
  if (!apiKey) {
962
+ showLoadingState('Please enter your Google Cloud API key');
 
 
 
 
 
 
963
  return;
964
  }
965
 
966
+ showLoadingState('Loading Google TTS voices...');
 
 
 
 
 
967
 
968
  try {
969
  const languageCode = languageSelect.value;
 
998
  availableVoices = naturalVoices;
999
 
1000
  if (naturalVoices.length === 0) {
1001
+ showErrorState('No high-quality voices available for selected language');
 
 
 
 
 
1002
  return;
1003
  }
1004
 
1005
+ // Create categorized voice interface
1006
+ createVoiceInterface(naturalVoices);
1007
 
1008
+ // Auto-select default male voice
1009
+ selectDefaultMaleVoice();
 
 
 
 
 
 
 
 
 
 
1010
  updateCostEstimator();
1011
 
1012
+ showToast(`Loaded ${naturalVoices.length} high-quality voices`, 'success');
1013
+
1014
  } catch (error) {
1015
  console.error('Error loading voices:', error);
1016
+ showErrorState(error.message);
1017
+ }
1018
+ }
1019
+
1020
+ function showLoadingState(message) {
1021
+ [maleVoices, femaleVoices, neutralVoices].forEach(container => {
1022
+ container.innerHTML = `
1023
+ <div class="text-center py-4">
1024
+ <div class="loading-spinner mx-auto mb-2"></div>
1025
+ <p class="text-gray-500 text-sm">${message}</p>
1026
  </div>
1027
  `;
1028
+ });
 
 
1029
  }
1030
 
1031
+ function showErrorState(message) {
1032
+ [maleVoices, femaleVoices, neutralVoices].forEach(container => {
1033
+ container.innerHTML = `
1034
+ <div class="text-center py-4">
1035
+ <i class="fas fa-exclamation-triangle text-red-400 text-lg mb-2"></i>
1036
+ <p class="text-red-500 text-sm">${message}</p>
1037
+ </div>
1038
+ `;
1039
+ });
1040
+ }
1041
+
1042
+ // Create categorized voice interface
1043
+ function createVoiceInterface(voices) {
1044
+ const male = voices.filter(v => v.ssmlGender === 'MALE');
1045
+ const female = voices.filter(v => v.ssmlGender === 'FEMALE');
1046
+ const neutral = voices.filter(v => v.ssmlGender === 'NEUTRAL');
1047
+
1048
+ maleVoices.innerHTML = male.length ? male.map(voice => createVoiceOption(voice)).join('') : '<p class="text-gray-500 text-sm">No male voices available</p>';
1049
+ femaleVoices.innerHTML = female.length ? female.map(voice => createVoiceOption(voice)).join('') : '<p class="text-gray-500 text-sm">No female voices available</p>';
1050
+ neutralVoices.innerHTML = neutral.length ? neutral.map(voice => createVoiceOption(voice)).join('') : '<p class="text-gray-500 text-sm">No neutral voices available</p>';
1051
+
1052
+ // Add click listeners to all voice options
1053
+ document.querySelectorAll('.voice-option').forEach(option => {
1054
+ option.addEventListener('click', () => {
1055
+ const voiceName = option.dataset.voiceName;
1056
+ const voice = voices.find(v => v.name === voiceName);
1057
+ if (voice) {
1058
+ selectVoice(voice);
1059
+ }
1060
+ });
1061
+ });
1062
+ }
1063
+
1064
+ function createVoiceOption(voice) {
1065
+ const voiceType = getVoiceTypeAndPricing(voice.name);
1066
+ const displayName = voice.name.replace(/-/g, ' ').replace(/en US /i, '').replace(/Wavenet|Neural2|Studio/i, '');
1067
 
1068
+ return `
1069
+ <div class="voice-option p-3 border border-gray-200 rounded-lg cursor-pointer" data-voice-name="${voice.name}">
1070
+ <div class="flex justify-between items-center">
1071
+ <div>
1072
+ <div class="font-medium text-gray-800">${displayName}</div>
1073
+ <div class="text-xs text-gray-500">${voiceType.name} • ${voice.languageCodes[0]}</div>
1074
+ </div>
1075
+ <div class="text-right">
1076
+ <div class="text-xs text-blue-600">$${voiceType.rate}/1M</div>
1077
+ <button class="sample-btn text-xs text-gray-600 hover:text-blue-600 mt-1">
1078
+ <i class="fas fa-play text-xs"></i> Sample
1079
+ </button>
1080
+ </div>
1081
+ </div>
1082
+ </div>
1083
+ `;
1084
+ }
1085
+
1086
+ function selectVoice(voice) {
1087
+ selectedVoice = voice;
1088
+ selectedVoiceName.textContent = voice.name;
1089
 
1090
+ // Update UI selection state
1091
+ document.querySelectorAll('.voice-option').forEach(option => {
1092
+ option.classList.remove('selected');
 
 
 
1093
  });
1094
+ const selectedOption = document.querySelector(`[data-voice-name="${voice.name}"]`);
1095
+ if (selectedOption) {
1096
+ selectedOption.classList.add('selected');
1097
+ }
1098
 
1099
+ updateCostEstimator();
1100
+ showToast(`Selected voice: ${voice.name}`, 'success', 1500);
1101
+
1102
+ // Add sample button click listener
1103
+ const sampleBtn = selectedOption?.querySelector('.sample-btn');
1104
+ if (sampleBtn) {
1105
+ sampleBtn.addEventListener('click', (e) => {
1106
+ e.stopPropagation();
1107
+ playSample(voice);
1108
+ });
1109
+ }
1110
+ }
1111
+
1112
+ // Play voice sample
1113
+ async function playSample(voice) {
1114
+ if (!apiKey) {
1115
+ showToast('Please enter your API key first', 'error');
1116
+ return;
1117
+ }
1118
 
1119
+ if (isPlaying) {
1120
+ stopPlayback();
 
 
 
1121
  }
1122
+
1123
+ try {
1124
+ showSynthesisProgress(0);
1125
+ const sampleText = "Hello, this is a sample of my voice. I can read your documents with natural sounding speech.";
1126
+ const audioData = await synthesizeSpeech(sampleText, voice, 1, 0, 'wavenet');
1127
+ hideSynthesisProgress();
1128
+ playAudioData(audioData);
1129
+ } catch (error) {
1130
+ hideSynthesisProgress();
1131
+ console.error('Error playing sample:', error);
1132
+ showToast('Failed to play sample: ' + error.message, 'error');
1133
+ }
1134
+ }
1135
+
1136
+ // Show/hide synthesis progress
1137
+ function showSynthesisProgress(progress) {
1138
+ synthesisProgress.classList.remove('hidden');
1139
+ progressPercentage.textContent = `${Math.round(progress)}%`;
1140
+ }
1141
+
1142
+ function hideSynthesisProgress() {
1143
+ synthesisProgress.classList.add('hidden');
1144
  }
1145
 
1146
+ // Synthesize speech with progress tracking
1147
  async function synthesizeSpeech(text, voice, rate = 1, pitch = 0, model = 'standard') {
1148
  if (!apiKey) {
1149
  throw new Error('API key not provided');
 
1776
  // Playback controls
1777
  playBtn.addEventListener('click', async () => {
1778
  if (!selectedVoice) {
1779
+ showToast('Please select a voice first', 'error');
1780
  return;
1781
  }
1782
 
 
1802
  stopPlayback();
1803
  });
1804
 
1805
+ // Playback functions with enhanced progress tracking
1806
  async function startPlayback() {
1807
  try {
1808
  isPlaying = true;
 
1819
 
1820
  let allAudioData = [];
1821
 
1822
+ // Show synthesis progress
1823
+ showSynthesisProgress(0);
1824
+
1825
  for (let i = 0; i < chunks.length; i++) {
1826
  if (!isPlaying || isPaused) break;
1827
 
1828
+ const progress = (i / chunks.length) * 100;
1829
+ showSynthesisProgress(progress);
1830
+
1831
  const audioData = await synthesizeSpeech(
1832
  chunks[i],
1833
  selectedVoice,
 
1839
  allAudioData.push(audioData);
1840
  }
1841
 
1842
+ hideSynthesisProgress();
1843
+
1844
  if (allAudioData.length > 0 && isPlaying) {
1845
  await combineAndPlayAudio(allAudioData);
1846
  }
1847
 
1848
  } catch (error) {
1849
+ hideSynthesisProgress();
1850
  console.error('Error during playback:', error);
1851
  showToast('Failed to play text: ' + error.message, 'error');
1852
  stopPlayback();
 
1933
  downloadBtn.disabled = !currentAudioBlob;
1934
 
1935
  if (isPlaying && !isPaused) {
1936
+ playBtn.innerHTML = '<i class="fas fa-pause text-lg"></i>';
1937
  } else {
1938
+ playBtn.innerHTML = '<i class="fas fa-play text-lg"></i>';
1939
  }
1940
  }
1941