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

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +122 -220
index.html CHANGED
@@ -1,5 +1,14 @@
1
  <!DOCTYPE html>
2
  <html lang="en">
 
 
 
 
 
 
 
 
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -59,19 +68,6 @@
59
  border-radius: 3px;
60
  padding: 0 2px;
61
  }
62
- .voice-card {
63
- transition: all 0.2s ease;
64
- border: 1px solid var(--border-color);
65
- background-color: var(--card-bg);
66
- }
67
- .voice-card:hover {
68
- border-color: #3B82F6;
69
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
70
- }
71
- .voice-card.selected {
72
- border-color: #3B82F6;
73
- background-color: #EFF6FF;
74
- }
75
  .loading-spinner {
76
  border: 3px solid rgba(255, 255, 255, 0.3);
77
  border-radius: 50%;
@@ -118,33 +114,6 @@
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);
@@ -178,6 +147,11 @@
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,7 +248,7 @@
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>
@@ -319,41 +293,19 @@
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
 
@@ -385,10 +337,10 @@
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>
@@ -398,8 +350,8 @@
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">
@@ -471,16 +423,18 @@
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
@@ -503,9 +457,6 @@
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,7 +466,10 @@
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');
@@ -577,6 +531,13 @@
577
  pitchSelect.addEventListener('change', updateCostEstimator);
578
 
579
  document.getElementById('showCostBreakdown')?.addEventListener('click', showCostBreakdownModal);
 
 
 
 
 
 
 
580
  }
581
 
582
  // Voice type and pricing functions
@@ -643,29 +604,33 @@
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;
@@ -687,8 +652,9 @@
687
  case 'neural2':
688
  preferredVoices = availableVoices.filter(v => v.name.includes('Neural2'));
689
  break;
690
- default:
691
- preferredVoices = availableVoices.filter(v => !v.name.includes('Studio') && !v.name.includes('Wavenet') && !v.name.includes('Neural2'));
 
692
  }
693
 
694
  if (preferredVoices.length > 0) {
@@ -716,7 +682,7 @@
716
  <div class="space-y-3">
717
  <div class="bg-green-50 p-3 rounded border border-green-200">
718
  <div class="flex justify-between items-center">
719
- <span class="font-medium text-green-800">Standard Voices</span>
720
  <span class="text-green-900 font-bold">$4.00</span>
721
  </div>
722
  <div class="text-sm text-green-600">per million characters</div>
@@ -907,6 +873,23 @@
907
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
908
  }
909
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
910
  // API Key Management
911
  saveKeyBtn.addEventListener('click', async () => {
912
  const key = apiKeyInput.value ? apiKeyInput.value.trim() : '';
@@ -959,11 +942,15 @@
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;
@@ -987,126 +974,40 @@
987
  throw new Error('Invalid response from Google TTS API - no voices returned');
988
  }
989
 
990
- // Filter for natural HD voices and store them
991
- const naturalVoices = data.voices.filter(voice =>
992
- voice.name.includes('Wavenet') ||
993
- voice.name.includes('Studio') ||
994
- voice.name.includes('Neural2')
995
- );
996
-
997
- naturalVoices.sort((a, b) => a.name.localeCompare(b.name));
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
@@ -1123,7 +1024,7 @@
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) {
@@ -1748,7 +1649,7 @@
1748
  for (let i = 1; i <= Math.min(5, lines.length - 1); i++) {
1749
  const values = lines[i].split(',').map(v => v.trim().replace(/"/g, ''));
1750
  text += `Row ${i}: ${values.join(', ')}\n`;
1751
- }
1752
  }
1753
 
1754
  setDocumentContent(text);
@@ -1771,12 +1672,13 @@
1771
  }
1772
 
1773
  updateCostEstimator();
 
1774
  }
1775
 
1776
  // Playback controls
1777
  playBtn.addEventListener('click', async () => {
1778
  if (!selectedVoice) {
1779
- showToast('Please select a voice first', 'error');
1780
  return;
1781
  }
1782
 
 
1
  <!DOCTYPE html>
2
  <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>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>
10
+ :root {<!DOCTYPE html>
11
+ <html lang="en">
12
  <head>
13
  <meta charset="UTF-8">
14
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 
68
  border-radius: 3px;
69
  padding: 0 2px;
70
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  .loading-spinner {
72
  border: 3px solid rgba(255, 255, 255, 0.3);
73
  border-radius: 50%;
 
114
  transition: all 0.2s;
115
  }
116
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  /* Audio controls enhancement */
118
  .audio-controls {
119
  background: linear-gradient(145deg, #ffffff, #f9fafb);
 
147
  background: linear-gradient(45deg, #3b82f6, #6366f1);
148
  box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3);
149
  }
150
+
151
+ .voice-status {
152
+ background: linear-gradient(145deg, #f0f9ff, #e0f2fe);
153
+ border: 1px solid #0ea5e9;
154
+ }
155
  </style>
156
  </head>
157
  <body class="bg-gray-50 min-h-screen">
 
248
  </div>
249
  </div>
250
 
251
+ <!-- Voice & Settings Section -->
252
  <div class="border-t border-gray-200 p-6 bg-gray-50">
253
  <div class="flex justify-between items-center mb-4">
254
  <h2 class="text-xl font-semibold text-gray-800">Voice & Settings</h2>
 
293
  </div>
294
  </div>
295
 
296
+ <!-- Voice Status Display -->
297
+ <div id="voiceStatus" class="voice-status p-4 rounded-lg mb-6">
298
+ <div class="flex items-center justify-between">
299
+ <div class="flex items-center">
300
+ <i class="fas fa-microphone text-blue-600 mr-3"></i>
301
+ <div>
302
+ <div class="font-medium text-gray-800" id="currentVoiceName">Loading voices...</div>
303
+ <div class="text-sm text-gray-600" id="currentVoiceDetails">Please wait while voices are loaded</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  </div>
305
  </div>
306
+ <button id="playVoiceSample" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm transition" disabled>
307
+ <i class="fas fa-play mr-1"></i> Play Sample
308
+ </button>
309
  </div>
310
  </div>
311
 
 
337
  <div>
338
  <label for="modelSelect" class="block text-sm font-medium text-gray-700 mb-1">Voice Model</label>
339
  <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">
340
+ <option value="standard" selected>Standard - $4.00/1M chars (Default)</option>
341
+ <option value="wavenet">WaveNet - $16.00/1M chars</option>
342
+ <option value="neural2">Neural2 - $16.00/1M chars</option>
343
+ <option value="studio">Studio - $160.00/1M chars</option>
344
  </select>
345
  </div>
346
  </div>
 
350
  <div class="audio-controls p-6 m-4">
351
  <div class="flex flex-wrap justify-between items-center gap-4 mb-4">
352
  <div class="flex items-center gap-2">
353
+ <span class="text-sm font-medium text-gray-700">Ready to synthesize:</span>
354
+ <span id="synthesisStatus" class="text-sm text-blue-600 font-medium">Select voice and add text</span>
355
  </div>
356
 
357
  <div class="playback-controls flex items-center gap-3">
 
423
  studio: 160.00
424
  };
425
 
426
+ // Priority list for Standard male voices
427
+ const defaultStandardMaleVoices = [
 
 
 
 
 
428
  'en-US-Standard-A',
429
+ 'en-US-Standard-B',
430
+ 'en-US-Standard-D',
431
+ 'en-US-Standard-I',
432
+ 'en-GB-Standard-A',
433
+ 'en-GB-Standard-B',
434
+ 'en-GB-Standard-D',
435
+ 'en-AU-Standard-A',
436
+ 'en-AU-Standard-B',
437
+ 'en-AU-Standard-D'
438
  ];
439
 
440
  // Global error handler
 
457
  const fileInput = document.getElementById('fileInput');
458
  const documentContent = document.getElementById('documentContent');
459
  const charCount = document.getElementById('charCount');
 
 
 
460
  const refreshVoicesBtn = document.getElementById('refreshVoicesBtn');
461
  const languageSelect = document.getElementById('languageSelect');
462
  const rateSelect = document.getElementById('rateSelect');
 
466
  const pauseBtn = document.getElementById('pauseBtn');
467
  const stopBtn = document.getElementById('stopBtn');
468
  const downloadBtn = document.getElementById('downloadBtn');
469
+ const currentVoiceName = document.getElementById('currentVoiceName');
470
+ const currentVoiceDetails = document.getElementById('currentVoiceDetails');
471
+ const playVoiceSample = document.getElementById('playVoiceSample');
472
+ const synthesisStatus = document.getElementById('synthesisStatus');
473
  const currentTime = document.getElementById('currentTime');
474
  const totalTime = document.getElementById('totalTime');
475
  const progressContainer = document.getElementById('progressContainer');
 
531
  pitchSelect.addEventListener('change', updateCostEstimator);
532
 
533
  document.getElementById('showCostBreakdown')?.addEventListener('click', showCostBreakdownModal);
534
+
535
+ // Play voice sample event
536
+ playVoiceSample.addEventListener('click', () => {
537
+ if (selectedVoice) {
538
+ playSample(selectedVoice);
539
+ }
540
+ });
541
  }
542
 
543
  // Voice type and pricing functions
 
604
  }
605
  }
606
 
607
+ // Find and select default STANDARD male voice
608
+ function selectDefaultStandardMaleVoice() {
609
  if (!availableVoices.length) return;
610
 
611
+ // Only look for Standard voices
612
+ const standardVoices = availableVoices.filter(v => v.name.includes('Standard'));
613
+
614
+ // Try to find preferred male voices in order
615
+ for (const preferredVoice of defaultStandardMaleVoices) {
616
+ const voice = standardVoices.find(v => v.name === preferredVoice);
617
  if (voice) {
618
  selectVoice(voice);
619
  return voice;
620
  }
621
  }
622
 
623
+ // Fallback to first standard male voice
624
+ const standardMaleVoice = standardVoices.find(v => v.ssmlGender === 'MALE');
625
+ if (standardMaleVoice) {
626
+ selectVoice(standardMaleVoice);
627
+ return standardMaleVoice;
628
  }
629
 
630
+ // Final fallback to any standard voice
631
+ if (standardVoices.length > 0) {
632
+ selectVoice(standardVoices[0]);
633
+ return standardVoices[0];
634
  }
635
 
636
  return null;
 
652
  case 'neural2':
653
  preferredVoices = availableVoices.filter(v => v.name.includes('Neural2'));
654
  break;
655
+ case 'standard':
656
+ preferredVoices = availableVoices.filter(v => v.name.includes('Standard'));
657
+ break;
658
  }
659
 
660
  if (preferredVoices.length > 0) {
 
682
  <div class="space-y-3">
683
  <div class="bg-green-50 p-3 rounded border border-green-200">
684
  <div class="flex justify-between items-center">
685
+ <span class="font-medium text-green-800">Standard Voices (Default)</span>
686
  <span class="text-green-900 font-bold">$4.00</span>
687
  </div>
688
  <div class="text-sm text-green-600">per million characters</div>
 
873
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
874
  }
875
 
876
+ // Update synthesis status
877
+ function updateSynthesisStatus() {
878
+ if (!selectedVoice) {
879
+ synthesisStatus.textContent = 'Please select an API key to load voices';
880
+ return;
881
+ }
882
+
883
+ if (!currentText.trim()) {
884
+ synthesisStatus.textContent = 'Add text to synthesize';
885
+ return;
886
+ }
887
+
888
+ const voiceType = getVoiceTypeAndPricing(selectedVoice.name);
889
+ const estimation = calculateEstimatedCost(currentText.length, voiceType);
890
+ synthesisStatus.textContent = `Ready (${currentText.length} chars, ~$${estimation.cost.toFixed(4)})`;
891
+ }
892
+
893
  // API Key Management
894
  saveKeyBtn.addEventListener('click', async () => {
895
  const key = apiKeyInput.value ? apiKeyInput.value.trim() : '';
 
942
  // Load available voices from Google TTS
943
  async function loadVoices() {
944
  if (!apiKey) {
945
+ currentVoiceName.textContent = 'No API key provided';
946
+ currentVoiceDetails.textContent = 'Please enter your Google Cloud API key above';
947
+ playVoiceSample.disabled = true;
948
  return;
949
  }
950
 
951
+ currentVoiceName.textContent = 'Loading voices...';
952
+ currentVoiceDetails.textContent = 'Please wait while voices are loaded';
953
+ playVoiceSample.disabled = true;
954
 
955
  try {
956
  const languageCode = languageSelect.value;
 
974
  throw new Error('Invalid response from Google TTS API - no voices returned');
975
  }
976
 
977
+ // Store all voices for model switching
978
+ availableVoices = data.voices;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
979
 
980
+ // Always start with Standard voices (as per user request)
981
+ selectDefaultStandardMaleVoice();
982
  updateCostEstimator();
983
+ updateSynthesisStatus();
984
 
985
+ showToast(`Loaded ${availableVoices.length} voices successfully`, 'success');
986
 
987
  } catch (error) {
988
  console.error('Error loading voices:', error);
989
+ currentVoiceName.textContent = 'Error loading voices';
990
+ currentVoiceDetails.textContent = error.message;
991
+ showToast('Failed to load voices: ' + error.message, 'error');
992
  }
993
  }
994
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
995
  function selectVoice(voice) {
996
  selectedVoice = voice;
 
997
 
998
+ // Update voice display
999
+ const displayName = voice.name.replace(/en-US-|en-GB-|en-AU-/, '').replace(/-/g, ' ');
1000
+ currentVoiceName.textContent = displayName;
1001
+
1002
+ const voiceType = getVoiceTypeAndPricing(voice.name);
1003
+ const genderIcon = voice.ssmlGender === 'MALE' ? '♂' : voice.ssmlGender === 'FEMALE' ? '♀' : '○';
1004
+ currentVoiceDetails.textContent = `${genderIcon} ${voice.ssmlGender} • ${voiceType.name} Voice • $${voiceType.rate}/1M chars`;
 
1005
 
1006
+ playVoiceSample.disabled = false;
1007
  updateCostEstimator();
1008
+ updateSynthesisStatus();
1009
+
1010
+ showToast(`Selected: ${voice.name}`, 'success', 1500);
 
 
 
 
 
 
 
1011
  }
1012
 
1013
  // Play voice sample
 
1024
  try {
1025
  showSynthesisProgress(0);
1026
  const sampleText = "Hello, this is a sample of my voice. I can read your documents with natural sounding speech.";
1027
+ const audioData = await synthesizeSpeech(sampleText, voice, 1, 0, modelSelect.value);
1028
  hideSynthesisProgress();
1029
  playAudioData(audioData);
1030
  } catch (error) {
 
1649
  for (let i = 1; i <= Math.min(5, lines.length - 1); i++) {
1650
  const values = lines[i].split(',').map(v => v.trim().replace(/"/g, ''));
1651
  text += `Row ${i}: ${values.join(', ')}\n`;
1652
+ </div>
1653
  }
1654
 
1655
  setDocumentContent(text);
 
1672
  }
1673
 
1674
  updateCostEstimator();
1675
+ updateSynthesisStatus();
1676
  }
1677
 
1678
  // Playback controls
1679
  playBtn.addEventListener('click', async () => {
1680
  if (!selectedVoice) {
1681
+ showToast('Please wait for voices to load or check your API key', 'error');
1682
  return;
1683
  }
1684