Spaces:
Running
Running
Update index.html
Browse files- 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 |
-
<!--
|
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 |
-
<!--
|
323 |
-
<div class="voice-
|
324 |
-
<div class="
|
325 |
-
|
326 |
-
|
327 |
-
<
|
328 |
-
<
|
329 |
-
|
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="
|
389 |
-
<option value="
|
390 |
-
<option value="
|
391 |
-
<option value="
|
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">
|
402 |
-
<span id="
|
403 |
</div>
|
404 |
|
405 |
<div class="playback-controls flex items-center gap-3">
|
@@ -471,16 +423,18 @@
|
|
471 |
studio: 160.00
|
472 |
};
|
473 |
|
474 |
-
//
|
475 |
-
const
|
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
|
|
|
|
|
|
|
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
|
648 |
if (!availableVoices.length) return;
|
649 |
|
650 |
-
|
651 |
-
|
|
|
|
|
|
|
|
|
652 |
if (voice) {
|
653 |
selectVoice(voice);
|
654 |
return voice;
|
655 |
}
|
656 |
}
|
657 |
|
658 |
-
// Fallback to first male voice
|
659 |
-
const
|
660 |
-
if (
|
661 |
-
selectVoice(
|
662 |
-
return
|
663 |
}
|
664 |
|
665 |
-
// Final fallback to any voice
|
666 |
-
if (
|
667 |
-
selectVoice(
|
668 |
-
return
|
669 |
}
|
670 |
|
671 |
return null;
|
@@ -687,8 +652,9 @@
|
|
687 |
case 'neural2':
|
688 |
preferredVoices = availableVoices.filter(v => v.name.includes('Neural2'));
|
689 |
break;
|
690 |
-
|
691 |
-
preferredVoices = availableVoices.filter(v =>
|
|
|
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 |
-
|
|
|
|
|
963 |
return;
|
964 |
}
|
965 |
|
966 |
-
|
|
|
|
|
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 |
-
//
|
991 |
-
|
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 |
-
//
|
1009 |
-
|
1010 |
updateCostEstimator();
|
|
|
1011 |
|
1012 |
-
showToast(`Loaded ${
|
1013 |
|
1014 |
} catch (error) {
|
1015 |
console.error('Error loading voices:', error);
|
1016 |
-
|
|
|
|
|
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
|
1091 |
-
|
1092 |
-
|
1093 |
-
|
1094 |
-
const
|
1095 |
-
|
1096 |
-
|
1097 |
-
}
|
1098 |
|
|
|
1099 |
updateCostEstimator();
|
1100 |
-
|
1101 |
-
|
1102 |
-
|
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,
|
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
|
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 |
|