Spaces:
Running
Running
Update index.html
Browse files- index.html +145 -67
index.html
CHANGED
|
@@ -346,15 +346,30 @@
|
|
| 346 |
|
| 347 |
try {
|
| 348 |
// Validate API key by making a simple request
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
|
| 359 |
if (response.ok) {
|
| 360 |
apiKey = key;
|
|
@@ -362,17 +377,40 @@
|
|
| 362 |
showToast('API key validated and saved successfully', 'success');
|
| 363 |
loadVoices();
|
| 364 |
} else {
|
| 365 |
-
let errorMessage = '
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
if (response.status === 403) {
|
| 367 |
-
errorMessage = 'API key
|
| 368 |
} else if (response.status === 400) {
|
| 369 |
-
errorMessage = '
|
|
|
|
|
|
|
| 370 |
}
|
| 371 |
-
|
|
|
|
| 372 |
}
|
| 373 |
} catch (error) {
|
| 374 |
console.error('API key validation error:', error);
|
| 375 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
} finally {
|
| 377 |
// Reset button state
|
| 378 |
saveKeyBtn.innerHTML = '<i class="fas fa-save mr-2"></i> Save Key';
|
|
@@ -406,38 +444,59 @@
|
|
| 406 |
try {
|
| 407 |
const languageCode = languageSelect.value;
|
| 408 |
|
| 409 |
-
//
|
| 410 |
-
const
|
| 411 |
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 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('
|
| 435 |
} else if (response.status === 400) {
|
| 436 |
-
throw new Error(
|
| 437 |
} else if (response.status === 401) {
|
| 438 |
-
throw new Error('Unauthorized.
|
| 439 |
} else if (response.status === 404) {
|
| 440 |
-
throw new Error('API endpoint not found. Please
|
|
|
|
|
|
|
| 441 |
} else if (response.status >= 500) {
|
| 442 |
throw new Error('Google Cloud service error. Please try again later.');
|
| 443 |
} else {
|
|
@@ -448,35 +507,33 @@
|
|
| 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
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
<
|
| 465 |
-
<
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 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">${
|
| 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
|
|
@@ -617,6 +674,9 @@
|
|
| 617 |
throw new Error('No text provided for synthesis');
|
| 618 |
}
|
| 619 |
|
|
|
|
|
|
|
|
|
|
| 620 |
// Prepare voice configuration
|
| 621 |
let voiceConfig = {
|
| 622 |
languageCode: voice.languageCodes[0],
|
|
@@ -643,46 +703,64 @@
|
|
| 643 |
};
|
| 644 |
|
| 645 |
try {
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 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 |
-
|
| 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;
|
|
|
|
| 346 |
|
| 347 |
try {
|
| 348 |
// Validate API key by making a simple request
|
| 349 |
+
// Method 1: Try with header approach first
|
| 350 |
+
let response;
|
| 351 |
+
try {
|
| 352 |
+
response = await fetch(`https://texttospeech.googleapis.com/v1/voices?languageCode=en-US&pageSize=1`, {
|
| 353 |
+
method: 'GET',
|
| 354 |
+
headers: {
|
| 355 |
+
'X-Goog-Api-Key': key,
|
| 356 |
+
'Accept': 'application/json',
|
| 357 |
+
},
|
| 358 |
+
mode: 'cors',
|
| 359 |
+
cache: 'no-cache'
|
| 360 |
+
});
|
| 361 |
+
} catch (headerError) {
|
| 362 |
+
// Method 2: Fallback to URL parameter
|
| 363 |
+
const testUrl = `https://texttospeech.googleapis.com/v1/voices?key=${encodeURIComponent(key)}&languageCode=en-US&pageSize=1`;
|
| 364 |
+
response = await fetch(testUrl, {
|
| 365 |
+
method: 'GET',
|
| 366 |
+
headers: {
|
| 367 |
+
'Accept': 'application/json',
|
| 368 |
+
},
|
| 369 |
+
mode: 'cors',
|
| 370 |
+
cache: 'no-cache'
|
| 371 |
+
});
|
| 372 |
+
}
|
| 373 |
|
| 374 |
if (response.ok) {
|
| 375 |
apiKey = key;
|
|
|
|
| 377 |
showToast('API key validated and saved successfully', 'success');
|
| 378 |
loadVoices();
|
| 379 |
} else {
|
| 380 |
+
let errorMessage = 'API key validation failed';
|
| 381 |
+
let details = '';
|
| 382 |
+
|
| 383 |
+
try {
|
| 384 |
+
const errorData = await response.json();
|
| 385 |
+
errorMessage = errorData.error?.message || errorMessage;
|
| 386 |
+
details = errorData.error?.details ? ` (${JSON.stringify(errorData.error.details)})` : '';
|
| 387 |
+
} catch (e) {
|
| 388 |
+
details = ` (HTTP ${response.status})`;
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
if (response.status === 403) {
|
| 392 |
+
errorMessage = 'Access denied. Check: 1) API key validity, 2) Text-to-Speech API enabled, 3) Billing enabled, 4) Key restrictions';
|
| 393 |
} else if (response.status === 400) {
|
| 394 |
+
errorMessage = 'Bad request. API key format might be incorrect';
|
| 395 |
+
} else if (response.status === 429) {
|
| 396 |
+
errorMessage = 'Too many requests. Please wait a moment';
|
| 397 |
}
|
| 398 |
+
|
| 399 |
+
showToast(errorMessage + details, 'error', 5000);
|
| 400 |
}
|
| 401 |
} catch (error) {
|
| 402 |
console.error('API key validation error:', error);
|
| 403 |
+
let errorMessage = 'Failed to validate API key. ';
|
| 404 |
+
|
| 405 |
+
if (error.message.includes('Failed to fetch')) {
|
| 406 |
+
errorMessage += 'Please check your internet connection.';
|
| 407 |
+
} else if (error.message.includes('CORS')) {
|
| 408 |
+
errorMessage += 'CORS policy issue. Try with a different browser or disable extensions.';
|
| 409 |
+
} else {
|
| 410 |
+
errorMessage += error.message;
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
showToast(errorMessage, 'error', 5000);
|
| 414 |
} finally {
|
| 415 |
// Reset button state
|
| 416 |
saveKeyBtn.innerHTML = '<i class="fas fa-save mr-2"></i> Save Key';
|
|
|
|
| 444 |
try {
|
| 445 |
const languageCode = languageSelect.value;
|
| 446 |
|
| 447 |
+
// Clean and validate API key
|
| 448 |
+
const cleanApiKey = apiKey.trim();
|
| 449 |
|
| 450 |
+
// Method 1: Try with API key in header (preferred)
|
| 451 |
+
let response;
|
| 452 |
+
try {
|
| 453 |
+
response = await fetch(`https://texttospeech.googleapis.com/v1/voices?languageCode=${languageCode}`, {
|
| 454 |
+
method: 'GET',
|
| 455 |
+
headers: {
|
| 456 |
+
'X-Goog-Api-Key': cleanApiKey,
|
| 457 |
+
'Accept': 'application/json',
|
| 458 |
+
},
|
| 459 |
+
mode: 'cors',
|
| 460 |
+
cache: 'no-cache'
|
| 461 |
+
});
|
| 462 |
+
} catch (headerError) {
|
| 463 |
+
console.log('Header method failed, trying URL parameter method');
|
| 464 |
+
|
| 465 |
+
// Method 2: Fallback to API key in URL
|
| 466 |
+
const url = `https://texttospeech.googleapis.com/v1/voices?key=${encodeURIComponent(cleanApiKey)}&languageCode=${languageCode}`;
|
| 467 |
+
response = await fetch(url, {
|
| 468 |
+
method: 'GET',
|
| 469 |
+
headers: {
|
| 470 |
+
'Accept': 'application/json',
|
| 471 |
+
},
|
| 472 |
+
mode: 'cors',
|
| 473 |
+
cache: 'no-cache'
|
| 474 |
+
});
|
| 475 |
+
}
|
| 476 |
|
| 477 |
// Better error handling
|
| 478 |
if (!response.ok) {
|
| 479 |
let errorMessage = 'Unknown error occurred';
|
| 480 |
+
let detailedError = '';
|
| 481 |
+
|
| 482 |
try {
|
| 483 |
const errorData = await response.json();
|
| 484 |
errorMessage = errorData.error?.message || errorMessage;
|
| 485 |
+
detailedError = errorData.error?.details ? JSON.stringify(errorData.error.details) : '';
|
| 486 |
} catch (e) {
|
|
|
|
| 487 |
errorMessage = response.statusText || `HTTP ${response.status}`;
|
| 488 |
}
|
| 489 |
|
| 490 |
if (response.status === 403) {
|
| 491 |
+
throw new Error('Access denied. Please check: 1) API key is valid, 2) Text-to-Speech API is enabled, 3) Billing is enabled, 4) API key restrictions (if any) allow this domain.');
|
| 492 |
} else if (response.status === 400) {
|
| 493 |
+
throw new Error(`Bad request. The API key format might be incorrect. Details: ${detailedError || 'Please verify your API key is copied correctly with no extra spaces.'}`);
|
| 494 |
} else if (response.status === 401) {
|
| 495 |
+
throw new Error('Unauthorized. The API key is invalid or expired.');
|
| 496 |
} else if (response.status === 404) {
|
| 497 |
+
throw new Error('API endpoint not found. Please verify the Text-to-Speech API is enabled.');
|
| 498 |
+
} else if (response.status === 429) {
|
| 499 |
+
throw new Error('Too many requests. Please wait a moment and try again.');
|
| 500 |
} else if (response.status >= 500) {
|
| 501 |
throw new Error('Google Cloud service error. Please try again later.');
|
| 502 |
} else {
|
|
|
|
| 507 |
const data = await response.json();
|
| 508 |
|
| 509 |
if (!data || !data.voices) {
|
| 510 |
+
throw new Error('Invalid response from Google TTS API - no voices returned');
|
| 511 |
}
|
| 512 |
|
| 513 |
renderVoiceOptions(data.voices);
|
| 514 |
} catch (error) {
|
| 515 |
console.error('Error loading voices:', error);
|
| 516 |
|
| 517 |
+
let troubleshootingTips = `
|
| 518 |
+
<div class="mt-3 text-xs text-gray-600">
|
| 519 |
+
<p class="font-medium">Troubleshooting checklist:</p>
|
| 520 |
+
<ul class="mt-1 list-disc list-inside space-y-1">
|
| 521 |
+
<li>✓ Google Cloud project has billing enabled</li>
|
| 522 |
+
<li>✓ Text-to-Speech API is enabled in your project</li>
|
| 523 |
+
<li>✓ API key is unrestricted (or allows this domain)</li>
|
| 524 |
+
<li>✓ API key was copied without extra spaces</li>
|
| 525 |
+
<li>✓ API key permissions include Text-to-Speech API</li>
|
| 526 |
+
</ul>
|
| 527 |
+
<p class="mt-2 font-medium">Still having issues?</p>
|
| 528 |
+
<p class="text-gray-500">Try creating a new, unrestricted API key for testing.</p>
|
| 529 |
+
</div>
|
| 530 |
+
`;
|
|
|
|
|
|
|
| 531 |
|
| 532 |
voiceGrid.innerHTML = `
|
| 533 |
<div class="col-span-full text-center py-8">
|
| 534 |
<i class="fas fa-exclamation-triangle text-red-400 text-2xl"></i>
|
| 535 |
<p class="text-gray-500 mt-2">Failed to load voices</p>
|
| 536 |
+
<p class="text-red-500 text-sm mt-1 max-w-md mx-auto">${error.message}</p>
|
| 537 |
${troubleshootingTips}
|
| 538 |
<button id="retryVoices" class="mt-4 bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded text-sm">
|
| 539 |
<i class="fas fa-sync-alt mr-1"></i> Retry
|
|
|
|
| 674 |
throw new Error('No text provided for synthesis');
|
| 675 |
}
|
| 676 |
|
| 677 |
+
// Clean the API key
|
| 678 |
+
const cleanApiKey = apiKey.trim();
|
| 679 |
+
|
| 680 |
// Prepare voice configuration
|
| 681 |
let voiceConfig = {
|
| 682 |
languageCode: voice.languageCodes[0],
|
|
|
|
| 703 |
};
|
| 704 |
|
| 705 |
try {
|
| 706 |
+
// Method 1: Try with header approach first
|
| 707 |
+
let response;
|
| 708 |
+
try {
|
| 709 |
+
response = await fetch(`https://texttospeech.googleapis.com/v1/text:synthesize`, {
|
| 710 |
+
method: 'POST',
|
| 711 |
+
headers: {
|
| 712 |
+
'X-Goog-Api-Key': cleanApiKey,
|
| 713 |
+
'Content-Type': 'application/json',
|
| 714 |
+
'Accept': 'application/json',
|
| 715 |
+
},
|
| 716 |
+
mode: 'cors',
|
| 717 |
+
cache: 'no-cache',
|
| 718 |
+
body: JSON.stringify(request)
|
| 719 |
+
});
|
| 720 |
+
} catch (headerError) {
|
| 721 |
+
// Method 2: Fallback to URL parameter
|
| 722 |
+
const url = `https://texttospeech.googleapis.com/v1/text:synthesize?key=${encodeURIComponent(cleanApiKey)}`;
|
| 723 |
+
response = await fetch(url, {
|
| 724 |
+
method: 'POST',
|
| 725 |
+
headers: {
|
| 726 |
+
'Content-Type': 'application/json',
|
| 727 |
+
'Accept': 'application/json',
|
| 728 |
+
},
|
| 729 |
+
mode: 'cors',
|
| 730 |
+
cache: 'no-cache',
|
| 731 |
+
body: JSON.stringify(request)
|
| 732 |
+
});
|
| 733 |
+
}
|
| 734 |
|
| 735 |
if (!response.ok) {
|
| 736 |
let errorMessage = 'Failed to synthesize speech';
|
| 737 |
+
let details = '';
|
| 738 |
|
| 739 |
try {
|
| 740 |
const errorData = await response.json();
|
| 741 |
errorMessage = errorData.error?.message || errorMessage;
|
| 742 |
+
details = errorData.error?.details ? ` (${JSON.stringify(errorData.error.details)})` : '';
|
| 743 |
} catch (e) {
|
| 744 |
+
details = ` (HTTP ${response.status}: ${response.statusText})`;
|
| 745 |
}
|
| 746 |
|
| 747 |
if (response.status === 400) {
|
| 748 |
+
throw new Error('Invalid request parameters. Please check your text and settings.' + details);
|
| 749 |
} else if (response.status === 403) {
|
| 750 |
+
throw new Error('Access denied. Please check your API key and quota.' + details);
|
| 751 |
} else if (response.status === 429) {
|
| 752 |
+
throw new Error('Rate limit exceeded. Please wait a moment and try again.' + details);
|
| 753 |
} else if (response.status >= 500) {
|
| 754 |
+
throw new Error('Google Cloud service error. Please try again later.' + details);
|
| 755 |
} else {
|
| 756 |
+
throw new Error(errorMessage + details);
|
| 757 |
}
|
| 758 |
}
|
| 759 |
|
| 760 |
const data = await response.json();
|
| 761 |
|
| 762 |
if (!data || !data.audioContent) {
|
| 763 |
+
throw new Error('Invalid response from Google TTS API - no audio content received');
|
| 764 |
}
|
| 765 |
|
| 766 |
return data.audioContent;
|