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) {
|