Spaces:
Running
Running
Update index.html
Browse files- index.html +190 -41
index.html
CHANGED
|
@@ -99,9 +99,25 @@
|
|
| 99 |
<i class="fas fa-save mr-2"></i> Save Key
|
| 100 |
</button>
|
| 101 |
</div>
|
| 102 |
-
<
|
| 103 |
-
<
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
</div>
|
| 106 |
|
| 107 |
<!-- Input Section -->
|
|
@@ -321,13 +337,47 @@
|
|
| 321 |
document.head.appendChild(style);
|
| 322 |
|
| 323 |
// API Key Management
|
| 324 |
-
saveKeyBtn.addEventListener('click', () => {
|
| 325 |
const key = apiKeyInput.value.trim();
|
| 326 |
if (key) {
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
} else {
|
| 332 |
showToast('Please enter a valid API key', 'error');
|
| 333 |
}
|
|
@@ -355,29 +405,81 @@
|
|
| 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('
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
} else {
|
| 366 |
-
throw new Error(`API error
|
| 367 |
}
|
| 368 |
}
|
| 369 |
|
| 370 |
const data = await response.json();
|
| 371 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 372 |
} catch (error) {
|
| 373 |
console.error('Error loading voices:', error);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
voiceGrid.innerHTML = `
|
| 375 |
<div class="col-span-full text-center py-8">
|
| 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">${
|
| 379 |
-
|
| 380 |
-
|
|
|
|
| 381 |
</button>
|
| 382 |
</div>
|
| 383 |
`;
|
|
@@ -511,36 +613,83 @@
|
|
| 511 |
throw new Error('API key not provided');
|
| 512 |
}
|
| 513 |
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
speakingRate: rate,
|
| 524 |
-
pitch: pitch,
|
| 525 |
-
effectsProfileId: model === 'studio' ? ['telephony-class-application'] : undefined
|
| 526 |
-
}
|
| 527 |
};
|
| 528 |
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
});
|
| 536 |
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
}
|
| 541 |
|
| 542 |
-
const
|
| 543 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 544 |
}
|
| 545 |
|
| 546 |
// Play audio data
|
|
|
|
| 99 |
<i class="fas fa-save mr-2"></i> Save Key
|
| 100 |
</button>
|
| 101 |
</div>
|
| 102 |
+
<div class="mt-3 text-xs text-blue-600">
|
| 103 |
+
<p class="flex items-center mb-1">
|
| 104 |
+
<i class="fas fa-info-circle mr-1"></i> Your API key is stored locally and never sent to our servers.
|
| 105 |
+
</p>
|
| 106 |
+
<details class="mt-2">
|
| 107 |
+
<summary class="cursor-pointer hover:text-blue-800">Need help getting an API key? (Click to expand)</summary>
|
| 108 |
+
<div class="mt-2 text-xs text-gray-600 bg-white p-3 rounded border">
|
| 109 |
+
<p class="font-medium">How to get your Google Cloud API key:</p>
|
| 110 |
+
<ol class="mt-1 list-decimal list-inside space-y-1">
|
| 111 |
+
<li>Go to <a href="https://console.cloud.google.com" target="_blank" class="text-blue-600 hover:underline">Google Cloud Console</a></li>
|
| 112 |
+
<li>Create a new project or select existing one</li>
|
| 113 |
+
<li>Enable the "Cloud Text-to-Speech API"</li>
|
| 114 |
+
<li>Go to "Credentials" → "Create Credentials" → "API Key"</li>
|
| 115 |
+
<li>Copy the generated API key and paste it above</li>
|
| 116 |
+
<li>Optionally, restrict the key to Text-to-Speech API for security</li>
|
| 117 |
+
</ol>
|
| 118 |
+
</div>
|
| 119 |
+
</details>
|
| 120 |
+
</div>
|
| 121 |
</div>
|
| 122 |
|
| 123 |
<!-- Input Section -->
|
|
|
|
| 337 |
document.head.appendChild(style);
|
| 338 |
|
| 339 |
// API Key Management
|
| 340 |
+
saveKeyBtn.addEventListener('click', async () => {
|
| 341 |
const key = apiKeyInput.value.trim();
|
| 342 |
if (key) {
|
| 343 |
+
// Show loading state
|
| 344 |
+
saveKeyBtn.innerHTML = '<div class="loading-spinner inline-block w-4 h-4 mr-2"></div>Validating...';
|
| 345 |
+
saveKeyBtn.disabled = true;
|
| 346 |
+
|
| 347 |
+
try {
|
| 348 |
+
// Validate API key by making a simple request
|
| 349 |
+
const testUrl = `https://texttospeech.googleapis.com/v1/voices?key=${encodeURIComponent(key)}&languageCode=en-US&pageSize=1`;
|
| 350 |
+
const response = await fetch(testUrl, {
|
| 351 |
+
method: 'GET',
|
| 352 |
+
headers: {
|
| 353 |
+
'Accept': 'application/json',
|
| 354 |
+
},
|
| 355 |
+
mode: 'cors',
|
| 356 |
+
cache: 'no-cache'
|
| 357 |
+
});
|
| 358 |
+
|
| 359 |
+
if (response.ok) {
|
| 360 |
+
apiKey = key;
|
| 361 |
+
localStorage.setItem('googleTTSApiKey', key);
|
| 362 |
+
showToast('API key validated and saved successfully', 'success');
|
| 363 |
+
loadVoices();
|
| 364 |
+
} else {
|
| 365 |
+
let errorMessage = 'Invalid API key';
|
| 366 |
+
if (response.status === 403) {
|
| 367 |
+
errorMessage = 'API key invalid or Text-to-Speech API not enabled';
|
| 368 |
+
} else if (response.status === 400) {
|
| 369 |
+
errorMessage = 'Invalid API key format';
|
| 370 |
+
}
|
| 371 |
+
showToast(errorMessage, 'error');
|
| 372 |
+
}
|
| 373 |
+
} catch (error) {
|
| 374 |
+
console.error('API key validation error:', error);
|
| 375 |
+
showToast('Failed to validate API key. Please check your internet connection.', 'error');
|
| 376 |
+
} finally {
|
| 377 |
+
// Reset button state
|
| 378 |
+
saveKeyBtn.innerHTML = '<i class="fas fa-save mr-2"></i> Save Key';
|
| 379 |
+
saveKeyBtn.disabled = false;
|
| 380 |
+
}
|
| 381 |
} else {
|
| 382 |
showToast('Please enter a valid API key', 'error');
|
| 383 |
}
|
|
|
|
| 405 |
|
| 406 |
try {
|
| 407 |
const languageCode = languageSelect.value;
|
|
|
|
| 408 |
|
| 409 |
+
// Use different API endpoint format to avoid CORS issues
|
| 410 |
+
const url = `https://texttospeech.googleapis.com/v1/voices?key=${encodeURIComponent(apiKey)}&languageCode=${languageCode}`;
|
| 411 |
+
|
| 412 |
+
const response = await fetch(url, {
|
| 413 |
+
method: 'GET',
|
| 414 |
+
headers: {
|
| 415 |
+
'Accept': 'application/json',
|
| 416 |
+
'Content-Type': 'application/json',
|
| 417 |
+
},
|
| 418 |
+
mode: 'cors',
|
| 419 |
+
cache: 'no-cache'
|
| 420 |
+
});
|
| 421 |
+
|
| 422 |
+
// Better error handling
|
| 423 |
if (!response.ok) {
|
| 424 |
+
let errorMessage = 'Unknown error occurred';
|
| 425 |
+
try {
|
| 426 |
+
const errorData = await response.json();
|
| 427 |
+
errorMessage = errorData.error?.message || errorMessage;
|
| 428 |
+
} catch (e) {
|
| 429 |
+
// If response is not JSON, use status text
|
| 430 |
+
errorMessage = response.statusText || `HTTP ${response.status}`;
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
if (response.status === 403) {
|
| 434 |
+
throw new Error('Invalid API key or Text-to-Speech API not enabled. Please check your Google Cloud console.');
|
| 435 |
} else if (response.status === 400) {
|
| 436 |
+
throw new Error('Bad request. Please verify your API key format is correct.');
|
| 437 |
+
} else if (response.status === 401) {
|
| 438 |
+
throw new Error('Unauthorized. Please check your API key permissions.');
|
| 439 |
+
} else if (response.status === 404) {
|
| 440 |
+
throw new Error('API endpoint not found. Please check your API key validity.');
|
| 441 |
+
} else if (response.status >= 500) {
|
| 442 |
+
throw new Error('Google Cloud service error. Please try again later.');
|
| 443 |
} else {
|
| 444 |
+
throw new Error(`API error (${response.status}): ${errorMessage}`);
|
| 445 |
}
|
| 446 |
}
|
| 447 |
|
| 448 |
const data = await response.json();
|
| 449 |
+
|
| 450 |
+
if (!data || !data.voices) {
|
| 451 |
+
throw new Error('Invalid response from Google TTS API');
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
renderVoiceOptions(data.voices);
|
| 455 |
} catch (error) {
|
| 456 |
console.error('Error loading voices:', error);
|
| 457 |
+
|
| 458 |
+
let errorDisplay = error.message;
|
| 459 |
+
let troubleshootingTips = '';
|
| 460 |
+
|
| 461 |
+
if (error.message.includes('Invalid API key')) {
|
| 462 |
+
troubleshootingTips = `
|
| 463 |
+
<div class="mt-3 text-xs text-gray-600">
|
| 464 |
+
<p class="font-medium">Troubleshooting tips:</p>
|
| 465 |
+
<ul class="mt-1 list-disc list-inside space-y-1">
|
| 466 |
+
<li>Go to Google Cloud Console</li>
|
| 467 |
+
<li>Enable the Text-to-Speech API</li>
|
| 468 |
+
<li>Create API credentials</li>
|
| 469 |
+
<li>Copy the API key exactly</li>
|
| 470 |
+
</ul>
|
| 471 |
+
</div>
|
| 472 |
+
`;
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
voiceGrid.innerHTML = `
|
| 476 |
<div class="col-span-full text-center py-8">
|
| 477 |
<i class="fas fa-exclamation-triangle text-red-400 text-2xl"></i>
|
| 478 |
<p class="text-gray-500 mt-2">Failed to load voices</p>
|
| 479 |
+
<p class="text-red-500 text-sm mt-1">${errorDisplay}</p>
|
| 480 |
+
${troubleshootingTips}
|
| 481 |
+
<button id="retryVoices" class="mt-4 bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded text-sm">
|
| 482 |
+
<i class="fas fa-sync-alt mr-1"></i> Retry
|
| 483 |
</button>
|
| 484 |
</div>
|
| 485 |
`;
|
|
|
|
| 613 |
throw new Error('API key not provided');
|
| 614 |
}
|
| 615 |
|
| 616 |
+
if (!text || !text.trim()) {
|
| 617 |
+
throw new Error('No text provided for synthesis');
|
| 618 |
+
}
|
| 619 |
+
|
| 620 |
+
// Prepare voice configuration
|
| 621 |
+
let voiceConfig = {
|
| 622 |
+
languageCode: voice.languageCodes[0],
|
| 623 |
+
name: voice.name,
|
| 624 |
+
ssmlGender: voice.ssmlGender
|
|
|
|
|
|
|
|
|
|
|
|
|
| 625 |
};
|
| 626 |
|
| 627 |
+
// Prepare audio configuration
|
| 628 |
+
let audioConfig = {
|
| 629 |
+
audioEncoding: 'MP3',
|
| 630 |
+
speakingRate: Math.max(0.25, Math.min(4.0, rate)), // Clamp between 0.25 and 4.0
|
| 631 |
+
pitch: Math.max(-20.0, Math.min(20.0, pitch)), // Clamp between -20 and 20
|
| 632 |
+
};
|
|
|
|
| 633 |
|
| 634 |
+
// Add effects profile for studio quality
|
| 635 |
+
if (model === 'studio' && voice.name.includes('Studio')) {
|
| 636 |
+
audioConfig.effectsProfileId = ['telephony-class-application'];
|
| 637 |
}
|
| 638 |
|
| 639 |
+
const request = {
|
| 640 |
+
input: { text: text.trim() },
|
| 641 |
+
voice: voiceConfig,
|
| 642 |
+
audioConfig: audioConfig
|
| 643 |
+
};
|
| 644 |
+
|
| 645 |
+
try {
|
| 646 |
+
const url = `https://texttospeech.googleapis.com/v1/text:synthesize?key=${encodeURIComponent(apiKey)}`;
|
| 647 |
+
|
| 648 |
+
const response = await fetch(url, {
|
| 649 |
+
method: 'POST',
|
| 650 |
+
headers: {
|
| 651 |
+
'Content-Type': 'application/json',
|
| 652 |
+
'Accept': 'application/json',
|
| 653 |
+
},
|
| 654 |
+
mode: 'cors',
|
| 655 |
+
cache: 'no-cache',
|
| 656 |
+
body: JSON.stringify(request)
|
| 657 |
+
});
|
| 658 |
+
|
| 659 |
+
if (!response.ok) {
|
| 660 |
+
let errorMessage = 'Failed to synthesize speech';
|
| 661 |
+
|
| 662 |
+
try {
|
| 663 |
+
const errorData = await response.json();
|
| 664 |
+
errorMessage = errorData.error?.message || errorMessage;
|
| 665 |
+
} catch (e) {
|
| 666 |
+
errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
| 667 |
+
}
|
| 668 |
+
|
| 669 |
+
if (response.status === 400) {
|
| 670 |
+
throw new Error('Invalid request parameters. Please check your text and settings.');
|
| 671 |
+
} else if (response.status === 403) {
|
| 672 |
+
throw new Error('Access denied. Please check your API key and quota.');
|
| 673 |
+
} else if (response.status === 429) {
|
| 674 |
+
throw new Error('Rate limit exceeded. Please wait a moment and try again.');
|
| 675 |
+
} else if (response.status >= 500) {
|
| 676 |
+
throw new Error('Google Cloud service error. Please try again later.');
|
| 677 |
+
} else {
|
| 678 |
+
throw new Error(errorMessage);
|
| 679 |
+
}
|
| 680 |
+
}
|
| 681 |
+
|
| 682 |
+
const data = await response.json();
|
| 683 |
+
|
| 684 |
+
if (!data || !data.audioContent) {
|
| 685 |
+
throw new Error('Invalid response from Google TTS API');
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
return data.audioContent;
|
| 689 |
+
} catch (error) {
|
| 690 |
+
console.error('Synthesis error:', error);
|
| 691 |
+
throw error;
|
| 692 |
+
}
|
| 693 |
}
|
| 694 |
|
| 695 |
// Play audio data
|