Spaces:
Running
Running
Update index.html
Browse files- index.html +150 -48
index.html
CHANGED
|
@@ -92,8 +92,13 @@
|
|
| 92 |
<div class="flex flex-col md:flex-row items-center gap-4">
|
| 93 |
<div class="flex-1">
|
| 94 |
<label for="apiKey" class="block text-sm font-medium text-blue-800 mb-1">Google Cloud API Key</label>
|
| 95 |
-
<
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
</div>
|
| 98 |
<button id="saveKeyBtn" class="bg-blue-600 hover:bg-blue-700 text-white py-2 px-6 rounded-md transition whitespace-nowrap">
|
| 99 |
<i class="fas fa-save mr-2"></i> Save Key
|
|
@@ -115,6 +120,14 @@
|
|
| 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>
|
|
@@ -307,6 +320,20 @@
|
|
| 307 |
|
| 308 |
// Initialize AudioContext on user interaction
|
| 309 |
document.addEventListener('click', initAudioContext, { once: true });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
}
|
| 311 |
|
| 312 |
function initAudioContext() {
|
|
@@ -345,72 +372,123 @@
|
|
| 345 |
saveKeyBtn.disabled = true;
|
| 346 |
|
| 347 |
try {
|
| 348 |
-
//
|
| 349 |
-
|
| 350 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
try {
|
| 352 |
-
|
|
|
|
| 353 |
method: 'GET',
|
| 354 |
headers: {
|
| 355 |
-
'X-Goog-Api-Key':
|
| 356 |
'Accept': 'application/json',
|
| 357 |
},
|
| 358 |
mode: 'cors',
|
| 359 |
cache: 'no-cache'
|
| 360 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
} catch (headerError) {
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 372 |
}
|
| 373 |
|
| 374 |
-
if (
|
| 375 |
-
apiKey =
|
| 376 |
-
localStorage.setItem('googleTTSApiKey',
|
| 377 |
showToast('API key validated and saved successfully', 'success');
|
| 378 |
loadVoices();
|
| 379 |
} else {
|
| 380 |
-
|
| 381 |
-
|
| 382 |
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
errorMessage =
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
|
|
|
| 389 |
}
|
| 390 |
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
|
| 399 |
-
|
|
|
|
| 400 |
}
|
| 401 |
} catch (error) {
|
| 402 |
-
console.error('API key validation error:', error);
|
| 403 |
-
|
| 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';
|
|
@@ -421,6 +499,30 @@
|
|
| 421 |
}
|
| 422 |
});
|
| 423 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 424 |
// Load available voices from Google TTS
|
| 425 |
async function loadVoices() {
|
| 426 |
if (!apiKey) {
|
|
|
|
| 92 |
<div class="flex flex-col md:flex-row items-center gap-4">
|
| 93 |
<div class="flex-1">
|
| 94 |
<label for="apiKey" class="block text-sm font-medium text-blue-800 mb-1">Google Cloud API Key</label>
|
| 95 |
+
<div class="relative">
|
| 96 |
+
<input type="password" id="apiKey" placeholder="Enter your Google Cloud API key"
|
| 97 |
+
class="api-key-input w-full px-4 py-2 pr-10 border border-blue-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
| 98 |
+
<button type="button" id="toggleKeyVisibility" class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600">
|
| 99 |
+
<i class="fas fa-eye" id="eyeIcon"></i>
|
| 100 |
+
</button>
|
| 101 |
+
</div>
|
| 102 |
</div>
|
| 103 |
<button id="saveKeyBtn" class="bg-blue-600 hover:bg-blue-700 text-white py-2 px-6 rounded-md transition whitespace-nowrap">
|
| 104 |
<i class="fas fa-save mr-2"></i> Save Key
|
|
|
|
| 120 |
<li>Copy the generated API key and paste it above</li>
|
| 121 |
<li>Optionally, restrict the key to Text-to-Speech API for security</li>
|
| 122 |
</ol>
|
| 123 |
+
<div class="mt-3 p-2 bg-yellow-50 border border-yellow-200 rounded text-yellow-800">
|
| 124 |
+
<p class="font-medium">β οΈ Important:</p>
|
| 125 |
+
<ul class="mt-1 list-disc list-inside space-y-1">
|
| 126 |
+
<li>Make sure billing is enabled for your project</li>
|
| 127 |
+
<li>Copy ONLY the API key (no extra text)</li>
|
| 128 |
+
<li>If restricted, allow this domain: ${window.location.hostname}</li>
|
| 129 |
+
</ul>
|
| 130 |
+
</div>
|
| 131 |
</div>
|
| 132 |
</details>
|
| 133 |
</div>
|
|
|
|
| 320 |
|
| 321 |
// Initialize AudioContext on user interaction
|
| 322 |
document.addEventListener('click', initAudioContext, { once: true });
|
| 323 |
+
|
| 324 |
+
// API Key visibility toggle
|
| 325 |
+
const toggleKeyVisibility = document.getElementById('toggleKeyVisibility');
|
| 326 |
+
const eyeIcon = document.getElementById('eyeIcon');
|
| 327 |
+
|
| 328 |
+
toggleKeyVisibility.addEventListener('click', () => {
|
| 329 |
+
if (apiKeyInput.type === 'password') {
|
| 330 |
+
apiKeyInput.type = 'text';
|
| 331 |
+
eyeIcon.className = 'fas fa-eye-slash';
|
| 332 |
+
} else {
|
| 333 |
+
apiKeyInput.type = 'password';
|
| 334 |
+
eyeIcon.className = 'fas fa-eye';
|
| 335 |
+
}
|
| 336 |
+
});
|
| 337 |
}
|
| 338 |
|
| 339 |
function initAudioContext() {
|
|
|
|
| 372 |
saveKeyBtn.disabled = true;
|
| 373 |
|
| 374 |
try {
|
| 375 |
+
// Clean and validate the API key format
|
| 376 |
+
const cleanApiKey = key.trim();
|
| 377 |
+
|
| 378 |
+
// Basic validation of API key format
|
| 379 |
+
if (!cleanApiKey || cleanApiKey.length < 10) {
|
| 380 |
+
throw new Error('API key appears to be too short. Please verify you copied the complete key.');
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
// Check for common API key formats
|
| 384 |
+
if (!cleanApiKey.match(/^[A-Za-z0-9_-]+$/)) {
|
| 385 |
+
throw new Error('API key contains invalid characters. Please copy only the key without any extra text.');
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
// Multiple validation attempts with different methods
|
| 389 |
+
let validationErrors = [];
|
| 390 |
+
let validationSuccess = false;
|
| 391 |
+
|
| 392 |
+
// Method 1: Header authentication with minimal request
|
| 393 |
try {
|
| 394 |
+
console.log('Attempting validation with X-Goog-Api-Key header...');
|
| 395 |
+
const response = await fetch(`https://texttospeech.googleapis.com/v1/voices?languageCode=en-US&pageSize=1`, {
|
| 396 |
method: 'GET',
|
| 397 |
headers: {
|
| 398 |
+
'X-Goog-Api-Key': cleanApiKey,
|
| 399 |
'Accept': 'application/json',
|
| 400 |
},
|
| 401 |
mode: 'cors',
|
| 402 |
cache: 'no-cache'
|
| 403 |
});
|
| 404 |
+
|
| 405 |
+
if (response.ok) {
|
| 406 |
+
validationSuccess = true;
|
| 407 |
+
const data = await response.json();
|
| 408 |
+
console.log('Header method successful:', data);
|
| 409 |
+
} else {
|
| 410 |
+
const errorText = await response.text();
|
| 411 |
+
validationErrors.push(`Header method failed (${response.status}): ${errorText}`);
|
| 412 |
+
}
|
| 413 |
} catch (headerError) {
|
| 414 |
+
console.error('Header method error:', headerError);
|
| 415 |
+
validationErrors.push(`Header method failed: ${headerError.message}`);
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
// Method 2: URL parameter authentication if header failed
|
| 419 |
+
if (!validationSuccess) {
|
| 420 |
+
try {
|
| 421 |
+
console.log('Attempting validation with URL parameter...');
|
| 422 |
+
const testUrl = `https://texttospeech.googleapis.com/v1/voices?key=${encodeURIComponent(cleanApiKey)}&languageCode=en-US&pageSize=1`;
|
| 423 |
+
const response = await fetch(testUrl, {
|
| 424 |
+
method: 'GET',
|
| 425 |
+
headers: {
|
| 426 |
+
'Accept': 'application/json',
|
| 427 |
+
},
|
| 428 |
+
mode: 'cors',
|
| 429 |
+
cache: 'no-cache'
|
| 430 |
+
});
|
| 431 |
+
|
| 432 |
+
if (response.ok) {
|
| 433 |
+
validationSuccess = true;
|
| 434 |
+
const data = await response.json();
|
| 435 |
+
console.log('URL parameter method successful:', data);
|
| 436 |
+
} else {
|
| 437 |
+
const errorText = await response.text();
|
| 438 |
+
validationErrors.push(`URL parameter method failed (${response.status}): ${errorText}`);
|
| 439 |
+
}
|
| 440 |
+
} catch (urlError) {
|
| 441 |
+
console.error('URL parameter method error:', urlError);
|
| 442 |
+
validationErrors.push(`URL parameter method failed: ${urlError.message}`);
|
| 443 |
+
}
|
| 444 |
}
|
| 445 |
|
| 446 |
+
if (validationSuccess) {
|
| 447 |
+
apiKey = cleanApiKey;
|
| 448 |
+
localStorage.setItem('googleTTSApiKey', cleanApiKey);
|
| 449 |
showToast('API key validated and saved successfully', 'success');
|
| 450 |
loadVoices();
|
| 451 |
} else {
|
| 452 |
+
// Show detailed validation errors
|
| 453 |
+
console.error('All validation methods failed:', validationErrors);
|
| 454 |
|
| 455 |
+
let errorMessage = 'API key validation failed. ';
|
| 456 |
+
if (validationErrors.some(err => err.includes('API_KEY_INVALID'))) {
|
| 457 |
+
errorMessage += 'The API key is invalid or not recognized by Google.';
|
| 458 |
+
} else if (validationErrors.some(err => err.includes('403'))) {
|
| 459 |
+
errorMessage += 'Access denied - check billing and API enablement.';
|
| 460 |
+
} else {
|
| 461 |
+
errorMessage += 'Please check the key format and permissions.';
|
| 462 |
}
|
| 463 |
|
| 464 |
+
// Show comprehensive error details
|
| 465 |
+
const errorDetails = `
|
| 466 |
+
<div class="mt-3 text-xs text-gray-600 text-left">
|
| 467 |
+
<p class="font-medium mb-2">Validation attempts made:</p>
|
| 468 |
+
<ul class="list-disc list-inside space-y-1 mb-3">
|
| 469 |
+
${validationErrors.map(err => `<li>${err}</li>`).join('')}
|
| 470 |
+
</ul>
|
| 471 |
+
|
| 472 |
+
<p class="font-medium mb-2">API Key Checklist:</p>
|
| 473 |
+
<ol class="list-decimal list-inside space-y-1">
|
| 474 |
+
<li>β Copy ONLY the API key (no extra text or spaces)</li>
|
| 475 |
+
<li>β Go to Google Cloud Console β APIs & Services β Credentials</li>
|
| 476 |
+
<li>β Ensure Text-to-Speech API is enabled</li>
|
| 477 |
+
<li>β Verify billing is enabled for your project</li>
|
| 478 |
+
<li>β Check API key restrictions (if any)</li>
|
| 479 |
+
<li>β Try creating a new unrestricted key for testing</li>
|
| 480 |
+
</ol>
|
| 481 |
+
|
| 482 |
+
<p class="mt-3 text-red-600 font-medium">Debug info: Key length: ${cleanApiKey.length}</p>
|
| 483 |
+
</div>
|
| 484 |
+
`;
|
| 485 |
|
| 486 |
+
// Create a modal or expandable section for the error
|
| 487 |
+
showDetailedError(errorMessage, errorDetails);
|
| 488 |
}
|
| 489 |
} catch (error) {
|
| 490 |
+
console.error('General API key validation error:', error);
|
| 491 |
+
showToast(`Validation error: ${error.message}`, 'error', 5000);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
} finally {
|
| 493 |
// Reset button state
|
| 494 |
saveKeyBtn.innerHTML = '<i class="fas fa-save mr-2"></i> Save Key';
|
|
|
|
| 499 |
}
|
| 500 |
});
|
| 501 |
|
| 502 |
+
// Helper function to show detailed errors
|
| 503 |
+
function showDetailedError(message, details) {
|
| 504 |
+
const errorModal = document.createElement('div');
|
| 505 |
+
errorModal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';
|
| 506 |
+
errorModal.innerHTML = `
|
| 507 |
+
<div class="bg-white rounded-lg p-6 max-w-2xl max-h-96 overflow-y-auto">
|
| 508 |
+
<div class="flex justify-between items-start mb-4">
|
| 509 |
+
<h3 class="text-lg font-semibold text-red-600">API Key Validation Failed</h3>
|
| 510 |
+
<button class="text-gray-400 hover:text-gray-600" onclick="this.parentElement.parentElement.parentElement.remove()">
|
| 511 |
+
<i class="fas fa-times text-xl"></i>
|
| 512 |
+
</button>
|
| 513 |
+
</div>
|
| 514 |
+
<p class="text-gray-700 mb-4">${message}</p>
|
| 515 |
+
${details}
|
| 516 |
+
<div class="mt-6 flex justify-end">
|
| 517 |
+
<button class="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded" onclick="this.parentElement.parentElement.parentElement.remove()">
|
| 518 |
+
Close
|
| 519 |
+
</button>
|
| 520 |
+
</div>
|
| 521 |
+
</div>
|
| 522 |
+
`;
|
| 523 |
+
document.body.appendChild(errorModal);
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
// Load available voices from Google TTS
|
| 527 |
async function loadVoices() {
|
| 528 |
if (!apiKey) {
|