Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <meta name="description" content="Suno API Audio Generator with Hugging Face Secrets"> | |
| <title>🎵 Suno AI Audio Generator</title> | |
| <!-- Tailwind CSS for styling --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <style> | |
| body { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; | |
| } | |
| .gradient-bg { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| } | |
| .card { | |
| background: rgba(255, 255, 255, 0.95); | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| } | |
| .input-field { | |
| transition: all 0.3s ease; | |
| border: 2px solid #e5e7eb; | |
| } | |
| .input-field:focus { | |
| border-color: #667eea; | |
| box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| transition: all 0.3s ease; | |
| } | |
| .btn-primary:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4); | |
| } | |
| .btn-primary:disabled { | |
| opacity: 0.6; | |
| cursor: not-allowed; | |
| transform: none; | |
| box-shadow: none; | |
| } | |
| .spinner { | |
| border: 3px solid #f3f3f3; | |
| border-top: 3px solid #667eea; | |
| border-radius: 50%; | |
| width: 24px; | |
| height: 24px; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .copy-btn { | |
| transition: all 0.2s ease; | |
| } | |
| .copy-btn:hover { | |
| background-color: #f3f4f6; | |
| } | |
| .copy-btn.copied { | |
| background-color: #10b981 ; | |
| color: white; | |
| } | |
| pre { | |
| white-space: pre-wrap; | |
| word-wrap: break-word; | |
| font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; | |
| font-size: 14px; | |
| } | |
| </style> | |
| </head> | |
| <body class="flex items-center justify-center min-h-screen p-4"> | |
| <div class="card rounded-2xl shadow-2xl w-full max-w-2xl p-8"> | |
| <!-- Header --> | |
| <div class="text-center mb-8"> | |
| <h1 class="text-3xl font-bold bg-gradient-to-r from-purple-600 to-blue-600 bg-clip-text text-transparent mb-2"> | |
| 🎵 Suno AI Audio Generator | |
| </h1> | |
| <p class="text-gray-600"> | |
| Generate audio using Suno API with secure secret management | |
| </p> | |
| <div class="mt-2"> | |
| <span id="configStatus" class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium"> | |
| <span class="w-2 h-2 rounded-full bg-yellow-500 mr-2"></span> | |
| Loading configuration... | |
| </span> | |
| </div> | |
| </div> | |
| <!-- Configuration Section --> | |
| <div id="configSection" class="mb-6 p-4 bg-blue-50 rounded-lg border border-blue-200 hidden"> | |
| <h3 class="font-semibold text-blue-800 mb-2">🔒 Configuration Status</h3> | |
| <div class="space-y-2"> | |
| <div class="flex items-center"> | |
| <span id="apiKeyStatus" class="text-sm">API Key: <span class="font-mono">Loading...</span></span> | |
| <button onclick="copyToClipboard('apiKey')" | |
| class="copy-btn ml-2 px-2 py-1 text-xs bg-gray-100 rounded hover:bg-gray-200"> | |
| Copy | |
| </button> | |
| </div> | |
| <div class="flex items-center"> | |
| <span id="callbackStatus" class="text-sm">Callback URL: <span class="font-mono">Loading...</span></span> | |
| <button onclick="copyToClipboard('callbackUrl')" | |
| class="copy-btn ml-2 px-2 py-1 text-xs bg-gray-100 rounded hover:bg-gray-200"> | |
| Copy | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Form Section --> | |
| <div class="space-y-6"> | |
| <div> | |
| <label for="taskId" class="block text-sm font-medium text-gray-700 mb-2"> | |
| Task ID | |
| </label> | |
| <input type="text" | |
| id="taskId" | |
| placeholder="5c79****be8e" | |
| value="5c79****be8e" | |
| class="input-field w-full px-4 py-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| <p class="text-xs text-gray-500 mt-1">Enter your Suno API task ID</p> | |
| </div> | |
| <div> | |
| <label for="audioId" class="block text-sm font-medium text-gray-700 mb-2"> | |
| Audio ID | |
| </label> | |
| <input type="text" | |
| id="audioId" | |
| placeholder="e231****-****-****-****-****8cadc7dc" | |
| value="e231****-****-****-****-****8cadc7dc" | |
| class="input-field w-full px-4 py-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| <p class="text-xs text-gray-500 mt-1">Enter your Suno API audio ID</p> | |
| </div> | |
| <!-- Generate Button --> | |
| <button id="generateBtn" | |
| onclick="generateAudio()" | |
| class="btn-primary w-full py-3 rounded-lg text-white font-semibold flex items-center justify-center"> | |
| <span>🎵 Generate Audio</span> | |
| </button> | |
| <!-- Loading Indicator --> | |
| <div id="loading" class="hidden flex-col items-center justify-center p-4"> | |
| <div class="spinner mb-3"></div> | |
| <p class="text-gray-600">Generating audio... Please wait</p> | |
| </div> | |
| <!-- Status Messages --> | |
| <div id="status" class="hidden p-4 rounded-lg"></div> | |
| <!-- Result Section --> | |
| <div id="result" class="hidden"> | |
| <h3 class="font-semibold text-gray-700 mb-3">📋 Response:</h3> | |
| <div class="bg-gray-50 p-4 rounded-lg border border-gray-200"> | |
| <pre id="response" class="text-gray-800"></pre> | |
| </div> | |
| <div class="mt-4 flex justify-end"> | |
| <button onclick="copyToClipboard('response')" | |
| class="copy-btn px-4 py-2 bg-gray-100 rounded-lg hover:bg-gray-200"> | |
| 📋 Copy Response | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Footer --> | |
| <div class="mt-8 pt-6 border-t border-gray-200"> | |
| <p class="text-xs text-gray-500 text-center"> | |
| 🔒 Secrets are loaded securely from Hugging Face Space environment variables | |
| <br> | |
| <span id="envInfo" class="font-mono text-gray-600"></span> | |
| </p> | |
| </div> | |
| </div> | |
| <!-- Inline JavaScript for configuration --> | |
| <script> | |
| // Configuration object - will be populated from environment | |
| window.config = { | |
| apiKey: '', | |
| callbackUrl: '', | |
| isConfigured: false, | |
| envSource: 'Unknown', | |
| timestamp: new Date().toISOString() | |
| }; | |
| // Function to load configuration from multiple sources | |
| function loadConfiguration() { | |
| console.log('🔧 Loading configuration...'); | |
| // Method 1: Try to get from Hugging Face Space environment | |
| // Note: In Hugging Face Spaces, environment variables might not be directly accessible in browser | |
| // So we need to use fetch to a backend endpoint or URL parameters | |
| // For this example, we'll use URL parameters which can be set in Hugging Face Space settings | |
| const urlParams = new URLSearchParams(window.location.search); | |
| // Method 2: Try URL parameters (set in Hugging Face Space as query string) | |
| const apiKeyFromUrl = urlParams.get('api_key'); | |
| const callbackUrlFromUrl = urlParams.get('callback_url'); | |
| // Method 3: Try localStorage (for development/testing) | |
| const apiKeyFromStorage = localStorage.getItem('suno_api_key'); | |
| const callbackUrlFromStorage = localStorage.getItem('suno_callback_url'); | |
| // Priority: URL params > localStorage > empty | |
| if (apiKeyFromUrl && callbackUrlFromUrl) { | |
| window.config.apiKey = apiKeyFromUrl; | |
| window.config.callbackUrl = callbackUrlFromUrl; | |
| window.config.envSource = 'URL Parameters'; | |
| window.config.isConfigured = true; | |
| } else if (apiKeyFromStorage && callbackUrlFromStorage) { | |
| window.config.apiKey = apiKeyFromStorage; | |
| window.config.callbackUrl = callbackUrlFromStorage; | |
| window.config.envSource = 'Local Storage (Dev Mode)'; | |
| window.config.isConfigured = true; | |
| } else { | |
| // Method 4: Try to fetch from a backend endpoint | |
| fetch('/api/config') | |
| .then(response => { | |
| if (response.ok) return response.json(); | |
| throw new Error('No config endpoint'); | |
| }) | |
| .then(data => { | |
| window.config.apiKey = data.apiKey || ''; | |
| window.config.callbackUrl = data.callbackUrl || ''; | |
| window.config.envSource = 'Backend API'; | |
| window.config.isConfigured = !!window.config.apiKey; | |
| updateUI(); | |
| }) | |
| .catch(error => { | |
| console.log('No backend config available'); | |
| // Show configuration form | |
| showConfigForm(); | |
| }); | |
| } | |
| updateUI(); | |
| console.log('Configuration loaded:', window.config); | |
| } | |
| // Update UI based on configuration status | |
| function updateUI() { | |
| const configStatus = document.getElementById('configStatus'); | |
| const configSection = document.getElementById('configSection'); | |
| const apiKeyStatus = document.getElementById('apiKeyStatus'); | |
| const callbackStatus = document.getElementById('callbackStatus'); | |
| const envInfo = document.getElementById('envInfo'); | |
| if (window.config.isConfigured) { | |
| // Show success status | |
| configStatus.innerHTML = '<span class="w-2 h-2 rounded-full bg-green-500 mr-2"></span> Configured'; | |
| configStatus.className = 'inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800'; | |
| // Update config display | |
| apiKeyStatus.innerHTML = `API Key: <span class="font-mono">${maskString(window.config.apiKey, 8)}</span>`; | |
| callbackStatus.innerHTML = `Callback URL: <span class="font-mono">${window.config.callbackUrl}</span>`; | |
| envInfo.textContent = `Source: ${window.config.envSource}`; | |
| // Show config section | |
| configSection.classList.remove('hidden'); | |
| } else { | |
| // Show warning status | |
| configStatus.innerHTML = '<span class="w-2 h-2 rounded-full bg-red-500 mr-2"></span> Not Configured'; | |
| configStatus.className = 'inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800'; | |
| envInfo.textContent = 'Please configure secrets in Hugging Face Space settings'; | |
| } | |
| } | |
| // Helper function to mask sensitive strings | |
| function maskString(str, visibleChars = 4) { | |
| if (!str || str.length <= visibleChars * 2) return '••••••••'; | |
| const first = str.substring(0, visibleChars); | |
| const last = str.substring(str.length - visibleChars); | |
| return `${first}••••${last}`; | |
| } | |
| // Copy to clipboard helper | |
| function copyToClipboard(elementId) { | |
| let text = ''; | |
| if (elementId === 'apiKey') { | |
| text = window.config.apiKey; | |
| } else if (elementId === 'callbackUrl') { | |
| text = window.config.callbackUrl; | |
| } else if (elementId === 'response') { | |
| text = document.getElementById('response').textContent; | |
| } | |
| navigator.clipboard.writeText(text).then(() => { | |
| const btn = event.target; | |
| const originalText = btn.innerHTML; | |
| btn.innerHTML = '✅ Copied!'; | |
| btn.classList.add('copied'); | |
| setTimeout(() => { | |
| btn.innerHTML = originalText; | |
| btn.classList.remove('copied'); | |
| }, 2000); | |
| }); | |
| } | |
| // Show configuration form (for manual setup) | |
| function showConfigForm() { | |
| // Create a modal for manual configuration | |
| const modal = document.createElement('div'); | |
| modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50'; | |
| modal.innerHTML = ` | |
| <div class="bg-white rounded-xl p-6 max-w-md w-full"> | |
| <h3 class="text-lg font-semibold text-gray-800 mb-4">⚙️ Manual Configuration</h3> | |
| <p class="text-sm text-gray-600 mb-4">Enter your Suno API credentials:</p> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">API Key</label> | |
| <input type="password" | |
| id="manualApiKey" | |
| class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" | |
| placeholder="Bearer token..."> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Callback URL</label> | |
| <input type="text" | |
| id="manualCallbackUrl" | |
| class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" | |
| placeholder="https://api.example.com/callback"> | |
| </div> | |
| </div> | |
| <div class="flex gap-2 mt-6"> | |
| <button onclick="saveManualConfig()" class="flex-1 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"> | |
| Save | |
| </button> | |
| <button onclick="closeModal(this)" class="flex-1 py-2 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300"> | |
| Cancel | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| document.body.appendChild(modal); | |
| } | |
| function saveManualConfig() { | |
| const apiKey = document.getElementById('manualApiKey').value; | |
| const callbackUrl = document.getElementById('manualCallbackUrl').value; | |
| if (apiKey && callbackUrl) { | |
| // Save to localStorage for development | |
| localStorage.setItem('suno_api_key', apiKey); | |
| localStorage.setItem('suno_callback_url', callbackUrl); | |
| // Update global config | |
| window.config.apiKey = apiKey; | |
| window.config.callbackUrl = callbackUrl; | |
| window.config.envSource = 'Manual Setup'; | |
| window.config.isConfigured = true; | |
| updateUI(); | |
| closeModal(); | |
| } else { | |
| alert('Please fill in both fields'); | |
| } | |
| } | |
| function closeModal(btn) { | |
| const modal = btn.closest('.fixed'); | |
| if (modal) modal.remove(); | |
| } | |
| // Initialize configuration when page loads | |
| document.addEventListener('DOMContentLoaded', loadConfiguration); | |
| </script> | |
| <!-- External JavaScript for API calls --> | |
| <script src="script.js"></script> | |
| </body> | |
| </html> |