Spaces:
Runtime error
Runtime error
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Music Generation Tool</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --primary: #2dd4bf; | |
| --primary-dark: #1da896; | |
| --secondary: #f8fafc; | |
| --accent: #fbbf24; | |
| --text: #1e293b; | |
| --text-light: #64748b; | |
| --border: #e2e8f0; | |
| --error: #ef4444; | |
| --success: #22c55e; | |
| --warning: #f59e0b; | |
| --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); | |
| --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1); | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: #f1f5f9; | |
| color: var(--text); | |
| line-height: 1.6; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| } | |
| header { | |
| background-color: white; | |
| padding: 15px 0; | |
| box-shadow: var(--shadow); | |
| margin-bottom: 20px; | |
| border-radius: 12px; | |
| } | |
| .header-content { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .logo { | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| color: var(--primary); | |
| } | |
| .built-with { | |
| font-size: 0.875rem; | |
| color: var(--text-light); | |
| } | |
| .built-with a { | |
| color: var(--primary); | |
| text-decoration: none; | |
| } | |
| .main-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 20px; | |
| margin-bottom: 20px; | |
| } | |
| .card { | |
| background-color: white; | |
| border-radius: 12px; | |
| padding: 20px; | |
| box-shadow: var(--shadow); | |
| } | |
| h2 { | |
| font-size: 1.25rem; | |
| font-weight: 600; | |
| margin-bottom: 15px; | |
| color: var(--primary); | |
| border-bottom: 2px solid var(--primary); | |
| padding-bottom: 10px; | |
| } | |
| .form-group { | |
| margin-bottom: 15px; | |
| } | |
| label { | |
| display: block; | |
| margin-bottom: 5px; | |
| font-weight: 500; | |
| color: var(--text); | |
| } | |
| input[type="text"], | |
| input[type="email"], | |
| input[type="password"], | |
| textarea, | |
| select { | |
| width: 100%; | |
| padding: 10px; | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| font-family: inherit; | |
| font-size: 0.875rem; | |
| transition: border-color 0.2s; | |
| } | |
| input:focus, | |
| textarea:focus, | |
| select:focus { | |
| outline: none; | |
| border-color: var(--primary); | |
| box-shadow: 0 0 0 3px rgba(45, 212, 191, 0.1); | |
| } | |
| textarea { | |
| min-height: 100px; | |
| resize: vertical; | |
| } | |
| .btn { | |
| padding: 10px 15px; | |
| border: none; | |
| border-radius: 8px; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| font-size: 0.875rem; | |
| } | |
| .btn-primary { | |
| background-color: var(--primary); | |
| color: white; | |
| } | |
| .btn-primary:hover { | |
| background-color: var(--primary-dark); | |
| } | |
| .btn-secondary { | |
| background-color: var(--secondary); | |
| color: var(--text); | |
| border: 1px solid var(--border); | |
| } | |
| .btn-secondary:hover { | |
| background-color: #e2e8f0; | |
| } | |
| .btn-danger { | |
| background-color: var(--error); | |
| color: white; | |
| } | |
| .btn-danger:hover { | |
| background-color: #dc2626; | |
| } | |
| .btn-group { | |
| display: flex; | |
| gap: 10px; | |
| margin-bottom: 15px; | |
| } | |
| .output-area { | |
| background-color: #f8fafc; | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| padding: 15px; | |
| min-height: 100px; | |
| font-family: 'Courier New', monospace; | |
| font-size: 0.875rem; | |
| white-space: pre-wrap; | |
| word-wrap: break-word; | |
| margin-bottom: 15px; | |
| } | |
| .status-message { | |
| padding: 10px; | |
| border-radius: 8px; | |
| margin-bottom: 15px; | |
| font-size: 0.875rem; | |
| } | |
| .status-success { | |
| background-color: rgba(34, 197, 94, 0.1); | |
| color: var(--success); | |
| } | |
| .status-error { | |
| background-color: rgba(239, 68, 68, 0.1); | |
| color: var(--error); | |
| } | |
| .status-warning { | |
| background-color: rgba(245, 158, 11, 0.1); | |
| color: var(--warning); | |
| } | |
| .checkbox-group { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| margin-bottom: 10px; | |
| } | |
| .checkbox-group input[type="checkbox"] { | |
| width: auto; | |
| accent-color: var(--primary); | |
| } | |
| .file-upload { | |
| border: 2px dashed var(--border); | |
| border-radius: 8px; | |
| padding: 20px; | |
| text-align: center; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .file-upload:hover { | |
| border-color: var(--primary); | |
| background-color: rgba(45, 212, 191, 0.05); | |
| } | |
| .file-upload input[type="file"] { | |
| display: none; | |
| } | |
| .file-info { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| margin-top: 10px; | |
| padding: 10px; | |
| background-color: #f8fafc; | |
| border-radius: 8px; | |
| } | |
| .thumb-container { | |
| position: relative; | |
| margin-bottom: 10px; | |
| } | |
| .thumb { | |
| width: 100%; | |
| border-radius: 8px; | |
| box-shadow: var(--shadow); | |
| } | |
| .miniBtn { | |
| position: absolute; | |
| bottom: 10px; | |
| right: 10px; | |
| padding: 5px 10px; | |
| background-color: rgba(0, 0, 0, 0.7); | |
| color: white; | |
| border: none; | |
| border-radius: 4px; | |
| font-size: 0.75rem; | |
| cursor: pointer; | |
| } | |
| .processing-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(0, 0, 0, 0.5); | |
| display: none; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 1000; | |
| } | |
| .processing-content { | |
| background-color: white; | |
| padding: 30px; | |
| border-radius: 12px; | |
| text-align: center; | |
| max-width: 400px; | |
| } | |
| .spinner { | |
| border: 4px solid rgba(0, 0, 0, 0.1); | |
| border-radius: 50%; | |
| border-top: 4px solid var(--primary); | |
| width: 40px; | |
| height: 40px; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto 15px; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| @media (max-width: 768px) { | |
| .main-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| .header-content { | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="container"> | |
| <div class="header-content"> | |
| <div class="logo">Music Generation Tool</div> | |
| <div class="built-with"> | |
| Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank">anycoder</a> | |
| </div> | |
| </div> | |
| </div> | |
| </header> | |
| <div class="container"> | |
| <div class="main-grid"> | |
| <div class="card"> | |
| <h2>API Configuration</h2> | |
| <div class="form-group"> | |
| <label for="apiKey">OpenAI API Key</label> | |
| <input type="password" id="apiKey" placeholder="Enter your OpenAI API key"> | |
| </div> | |
| <div class="btn-group"> | |
| <button class="btn btn-primary" id="saveKeyBtn">Save Key</button> | |
| <button class="btn btn-secondary" id="testKeyBtn">Test Key</button> | |
| </div> | |
| <div id="apiStatus" class="status-message" style="display: none;"></div> | |
| <h2 style="margin-top: 25px;">Server Controls</h2> | |
| <div class="btn-group"> | |
| <button class="btn btn-secondary" id="checkHealthBtn">Check Server Health</button> | |
| <button class="btn btn-secondary" id="runDiagnosticsBtn">Run Diagnostics</button> | |
| </div> | |
| <div id="healthStatus" class="status-message" style="display: none;"></div> | |
| <div id="diagOut" class="output-area" style="display: none;"></div> | |
| </div> | |
| <div class="card"> | |
| <h2>Form Controls</h2> | |
| <div class="btn-group"> | |
| <button class="btn btn-secondary" id="randomizeBtn">Randomize Form</button> | |
| <button class="btn btn-secondary" id="resetFormBtn">Reset Form</button> | |
| </div> | |
| <div class="btn-group"> | |
| <button class="btn btn-secondary" id="saveDraftBtn">Save Draft</button> | |
| <button class="btn btn-secondary" id="loadDraftBtn">Load Draft</button> | |
| </div> | |
| <div id="warningsDisplay" class="status-message" style="display: none;"></div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h2>Main Form</h2> | |
| <div class="form-group"> | |
| <label for="songTitle">Song Title</label> | |
| <input type="text" id="songTitle" placeholder="Enter song title"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="occasion">Occasion</label> | |
| <input type="text" id="occasion" placeholder="e.g., Birthday, Anniversary"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="buyerName">Buyer Name</label> | |
| <input type="text" id="buyerName" placeholder="Enter buyer name"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="recipientName">Recipient Name</label> | |
| <input type="text" id="recipientName" placeholder="Enter recipient name"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="relationship">Relationship</label> | |
| <input type="text" id="relationship" placeholder="e.g., Mother, Friend"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="tone">Tone/Vibe</label> | |
| <input type="text" id="tone" placeholder="e.g., Happy, Romantic"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="mainGenre">Main Genre</label> | |
| <input type="text" id="mainGenre" placeholder="e.g., Pop, Rock"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="secondaryGenres">Secondary Genres</label> | |
| <input type="text" id="secondaryGenres" placeholder="e.g., Jazz, Electronic"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="tempo">Tempo/Energy</label> | |
| <input type="text" id="tempo" placeholder="e.g., Fast, Slow"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="vocalStyle">Vocal Style</label> | |
| <input type="text" id="vocalStyle" placeholder="e.g., Male, Female, Choir"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="instrumentationNotes">Instrumentation Notes</label> | |
| <textarea id="instrumentationNotes" placeholder="Any specific instruments or arrangements"></textarea> | |
| </div> | |
| <div class="form-group"> | |
| <label for="keyFacts">Key Facts</label> | |
| <textarea id="keyFacts" placeholder="Important facts to include in the song"></textarea> | |
| </div> | |
| <div class="form-group"> | |
| <label for="stories">Stories</label> | |
| <textarea id="stories" placeholder="Any stories to include"></textarea> | |
| </div> | |
| <div class="form-group"> | |
| <label for="mainMessage">Main Message</label> | |
| <textarea id="mainMessage" placeholder="The main message of the song"></textarea> | |
| </div> | |
| <div class="form-group"> | |
| <label for="namesPlaces">Names/Places</label> | |
| <input type="text" id="namesPlaces" placeholder="Any specific names or places to mention"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="visualStyle">Visual Style</label> | |
| <input type="text" id="visualStyle" placeholder="e.g., Realistic, Cartoon"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="visualMood">Visual Mood</label> | |
| <input type="text" id="visualMood" placeholder="e.g., Bright, Dark"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="visualPlacesThemes">Visual Places/Themes</label> | |
| <input type="text" id="visualPlacesThemes" placeholder="e.g., Beach, Mountains"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="visualText">Visual Text</label> | |
| <input type="text" id="visualText" placeholder="Any text to include in visuals"> | |
| </div> | |
| <div class="checkbox-group"> | |
| <input type="checkbox" id="wantsArtwork"> | |
| <label for="wantsArtwork">Wants Artwork</label> | |
| </div> | |
| <div class="checkbox-group"> | |
| <input type="checkbox" id="wantsVideo"> | |
| <label for="wantsVideo">Wants Video</label> | |
| </div> | |
| <div class="btn-group"> | |
| <button class="btn btn-primary" id="generateBtn">Generate Suno Block</button> | |
| <button class="btn btn-secondary" id="copySunoBtn">Copy Suno Block</button> | |
| </div> | |
| <div class="form-group"> | |
| <label>Suno Block Output</label> | |
| <div id="sunoBlockOut" class="output-area">Suno block will appear here...</div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h2>Generated Prompts</h2> | |
| <div class="form-group"> | |
| <label for="songPrompt">Song Prompt</label> | |
| <textarea id="songPrompt" readonly></textarea> | |
| </div> | |
| <div class="form-group"> | |
| <label for="artPrompt">Artwork Prompt</label> | |
| <textarea id="artPrompt" readonly></textarea> | |
| </div> | |
| <div class="form-group"> | |
| <label for="videoPrompt">Video Prompt</label> | |
| <textarea id="videoPrompt" readonly></textarea> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h2>Image Generation</h2> | |
| <div class="btn-group"> | |
| <button class="btn btn-primary" id="generateImageBtn">Generate Image</button> | |
| </div> | |
| <div id="imgGrid" class="output-area" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px;"></div> | |
| </div> | |
| <div class="card"> | |
| <h2>Video Generation</h2> | |
| <div class="btn-group"> | |
| <button class="btn btn-primary" id="generateVideoBtn">Generate Video</button> | |
| </div> | |
| <div id="videoPreview" class="output-area"></div> | |
| <div id="videoOut" class="status-message"></div> | |
| </div> | |
| <div class="card"> | |
| <h2>File Upload</h2> | |
| <div class="form-group"> | |
| <label>Song File</label> | |
| <div class="file-upload" id="songFileUpload"> | |
| <i class="fas fa-upload fa-2x"></i> | |
| <p>Click to upload song file (MP3, WAV, M4A)</p> | |
| <input type="file" id="songFileInput" accept="audio/mp3,audio/wav,audio/m4a"> | |
| </div> | |
| <div id="songFileInfo" class="file-info" style="display: none;"> | |
| <i class="fas fa-file-audio"></i> | |
| <span id="songFileName"></span> | |
| <button class="btn btn-danger" id="removeSongBtn" style="padding: 5px 10px;">Remove</button> | |
| </div> | |
| </div> | |
| <div class="form-group"> | |
| <label>Video Clips (MP4)</label> | |
| <div class="file-upload" id="clipFilesUpload"> | |
| <i class="fas fa-upload fa-2x"></i> | |
| <p>Click to upload video clips (MP4)</p> | |
| <input type="file" id="clipFilesInput" accept="video/mp4" multiple> | |
| </div> | |
| <div id="clipQueueWrap" class="file-info"> | |
| <div class="small" style="margin-top:6px">No clips queued.</div> | |
| </div> | |
| <div class="btn-group"> | |
| <button class="btn btn-danger" id="clearClipsBtn">Clear All Clips</button> | |
| <button class="btn btn-primary" id="stitchClipsBtn">Stitch Clips</button> | |
| </div> | |
| </div> | |
| <div id="uploadOut" class="status-message"></div> | |
| </div> | |
| </div> | |
| <div class="processing-overlay" id="processingOverlay"> | |
| <div class="processing-content"> | |
| <div class="spinner"></div> | |
| <div id="processingMessage">Processing...</div> | |
| </div> | |
| </div> | |
| <script> | |
| // Constants | |
| const API_BASE = 'https://api.example.com'; // Replace with your actual API base URL | |
| let apiKey = ''; | |
| let currentOrderId = null; | |
| let songFile = null; | |
| let videoClips = []; | |
| // DOM Elements | |
| const warningsDisplay = document.getElementById('warningsDisplay'); | |
| const apiStatus = document.getElementById('apiStatus'); | |
| const healthStatus = document.getElementById('healthStatus'); | |
| const diagOut = document.getElementById('diagOut'); | |
| const sunoBlockOut = document.getElementById('sunoBlockOut'); | |
| const orderIdDisplay = document.getElementById('orderIdDisplay'); | |
| const processingOverlay = document.getElementById('processingOverlay'); | |
| const processingMessage = document.getElementById('processingMessage'); | |
| const songFileInput = document.getElementById('songFileInput'); | |
| const songFileUpload = document.getElementById('songFileUpload'); | |
| const songFileInfo = document.getElementById('songFileInfo'); | |
| const songFileName = document.getElementById('songFileName'); | |
| const removeSongBtn = document.getElementById('removeSongBtn'); | |
| const clipFilesInput = document.getElementById('clipFilesInput'); | |
| const clipFilesUpload = document.getElementById('clipFilesUpload'); | |
| const clipQueueWrap = document.getElementById('clipQueueWrap'); | |
| const clearClipsBtn = document.getElementById('clearClipsBtn'); | |
| const stitchClipsBtn = document.getElementById('stitchClipsBtn'); | |
| const uploadOut = document.getElementById('uploadOut'); | |
| // Initialize the app | |
| function init() { | |
| // Load saved API key | |
| const savedKey = localStorage.getItem('openaiApiKey'); | |
| if (savedKey) { | |
| apiKey = savedKey; | |
| document.getElementById('apiKey').value = '••••••••••••••••'; | |
| } | |
| // Set up event listeners | |
| document.getElementById('saveKeyBtn').addEventListener('click', saveApiKey); | |
| document.getElementById('testKeyBtn').addEventListener('click', testApiKey); | |
| document.getElementById('checkHealthBtn').addEventListener('click', checkServerHealth); | |
| document.getElementById('runDiagnosticsBtn').addEventListener('click', runDiagnostics); | |
| document.getElementById('randomizeBtn').addEventListener('click', randomizeForm); | |
| document.getElementById('resetFormBtn').addEventListener('click', resetForm); | |
| document.getElementById('saveDraftBtn').addEventListener('click', saveDraft); | |
| document.getElementById('loadDraftBtn').addEventListener('click', loadDraft); | |
| document.getElementById('generateBtn').addEventListener('click', generateSunoBlock); | |
| document.getElementById('copySunoBtn').addEventListener('click', copySunoBlock); | |
| document.getElementById('generateImageBtn').addEventListener('click', generateImage); | |
| document.getElementById('generateVideoBtn').addEventListener('click', generateVideo); | |
| songFileUpload.addEventListener('click', () => songFileInput.click()); | |
| songFileInput.addEventListener('change', handleSongFileUpload); | |
| removeSongBtn.addEventListener('click', removeSongFile); | |
| clipFilesUpload.addEventListener('click', () => clipFilesInput.click()); | |
| clipFilesInput.addEventListener('change', handleClipFilesUpload); | |
| clearClipsBtn.addEventListener('click', clearClips); | |
| stitchClipsBtn.addEventListener('click', stitchClips); | |
| // Initialize file upload areas | |
| updateFileUploadAreas(); | |
| } | |
| // Update file upload areas | |
| function updateFileUploadAreas() { | |
| // Song file upload | |
| songFileUpload.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| songFileUpload.style.borderColor = var(--primary); | |
| }); | |
| songFileUpload.addEventListener('dragleave', () => { | |
| songFileUpload.style.borderColor = var(--border); | |
| }); | |
| songFileUpload.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| songFileUpload.style.borderColor = var(--border); | |
| songFileInput.files = e.dataTransfer.files; | |
| handleSongFileUpload(); | |
| }); | |
| // Clip files upload | |
| clipFilesUpload.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| clipFilesUpload.style.borderColor = var(--primary); | |
| }); | |
| clipFilesUpload.addEventListener('dragleave', () => { | |
| clipFilesUpload.style.borderColor = var(--border); | |
| }); | |
| clipFilesUpload.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| clipFilesUpload.style.borderColor = var(--border); | |
| clipFilesInput.files = e.dataTransfer.files; | |
| handleClipFilesUpload(); | |
| }); | |
| } | |
| // Save API key | |
| function saveApiKey() { | |
| const keyInput = document.getElementById('apiKey'); | |
| const keyValue = keyInput.value.trim(); | |
| if (!keyValue) { | |
| showStatus(apiStatus, 'Please enter an API key', 'error'); | |
| return; | |
| } | |
| // Simple validation - should start with 'sk-' and be at least 40 characters | |
| if (!keyValue.startsWith('sk-') || keyValue.length < 40) { | |
| showStatus(apiStatus, 'Invalid API key format', 'error'); | |
| return; | |
| } | |
| apiKey = keyValue; | |
| localStorage.setItem('openaiApiKey', apiKey); | |
| keyInput.value = '••••••••••••••••'; | |
| showStatus(apiStatus, 'API key saved successfully!', 'success'); | |
| } | |
| // Test API key | |
| async function testApiKey() { | |
| if (!apiKey) { | |
| showStatus(apiStatus, 'Please save your API key first!', 'error'); | |
| return; | |
| } | |
| try { | |
| showProcessing('Testing API key...'); | |
| // Mock API call - replace with actual API endpoint | |
| const response = await fetch(`${API_BASE}/test-key`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${apiKey}` | |
| } | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json(); | |
| throw new Error(errorData.error || 'API key test failed'); | |
| } | |
| const result = await response.json(); | |
| showStatus(apiStatus, 'API key is valid!', 'success'); | |
| } catch (error) { | |
| showStatus(apiStatus, `API key test failed: ${error.message}`, 'error'); | |
| console.error('API key test error:', error); | |
| } finally { | |
| hideProcessing(); | |
| } | |
| } | |
| // Check server health | |
| async function checkServerHealth() { | |
| try { | |
| showProcessing('Checking server health...'); | |
| const response = await fetch(`${API_BASE}/health`); | |
| if (!response.ok) throw new Error('Server health check failed'); | |
| const healthData = await response.json(); | |
| showStatus(healthStatus, `Server is healthy! (${healthData.status})`, 'success'); | |
| } catch (error) { | |
| showStatus(healthStatus, `Server health check failed: ${error.message}`, 'error'); | |
| console.error('Health check error:', error); | |
| } finally { | |
| hideProcessing(); | |
| } | |
| } | |
| // Show status message | |
| function showStatus(element, message, type) { | |
| element.textContent = message; | |
| element.className = `status-message status-${type}`; | |
| element.style.display = 'block'; | |
| // Hide after 5 seconds | |
| setTimeout(() => { | |
| element.style.display = 'none'; | |
| }, 5000); | |
| } | |
| // Get form data | |
| function getFormData() { | |
| return { | |
| songTitle: document.getElementById('songTitle').value, | |
| occasion: document.getElementById('occasion').value, | |
| customerName: document.getElementById('buyerName').value, | |
| recipient_name: document.getElementById('recipientName').value, | |
| relationship: document.getElementById('relationship').value, | |
| tone_vibe: document.getElementById('tone').value, | |
| main_genre: document.getElementById('mainGenre').value, | |
| secondary_influences: document.getElementById('secondaryGenres').value, | |
| tempo_energy: document.getElementById('tempo').value, | |
| vocal_style: document.getElementById('vocalStyle').value, | |
| instrumentation_notes: document.getElementById('instrumentationNotes').value, | |
| language_level: document.getElementById('languageLevel').value, | |
| package_level: document.getElementById('packageLevel').value, | |
| key_facts: document.getElementById('keyFacts').value, | |
| stories: document.getElementById('stories').value, | |
| main_message: document.getElementById('mainMessage').value, | |
| names_places: document.getElementById('namesPlaces').value, | |
| visual_style: document.getElementById('visualStyle').value, | |
| visual_mood: document.getElementById('visualMood').value, | |
| visual_places_themes: document.getElementById('visualPlacesThemes').value, | |
| visual_text: document.getElementById('visualText').value, | |
| wants_artwork: document.getElementById('wantsArtwork').checked, | |
| wants_video: document.getElementById('wantsVideo').checked | |
| }; | |
| } | |
| // Generate Suno block | |
| async function generateSunoBlock() { | |
| const formData = getFormData(); | |
| // Basic validation | |
| if (!formData.songTitle || !formData.customerName || !formData.recipient_name) { | |
| showStatus(warningsDisplay, 'Please fill in required fields (Song Title, Buyer Name, Recipient Name)', 'error'); | |
| return; | |
| } | |
| try { | |
| showProcessing('Generating Suno block...'); | |
| // Mock API call - replace with actual API endpoint | |
| const response = await fetch(`${API_BASE}/generate`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'X-API-Key': apiKey | |
| }, | |
| body: JSON.stringify(formData) | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json(); | |
| throw new Error(errorData.error || 'Generation failed'); | |
| } | |
| const result = await response.json(); | |
| currentOrderId = result.orderId; | |
| orderIdDisplay.textContent = currentOrderId; | |
| // Display the Suno block | |
| sunoBlockOut.textContent = result.sunoBlock; | |
| // Generate prompts | |
| document.getElementById('songPrompt').value = result.songPrompt; | |
| document.getElementById('artPrompt').value = result.artPrompt; | |
| document.getElementById('videoPrompt').value = result.videoPrompt; | |
| showStatus(warningsDisplay, 'Suno block generated successfully!', 'success'); | |
| } catch (error) { | |
| showStatus(warningsDisplay, `Error generating Suno block: ${error.message}`, 'error'); | |
| console.error('Generation error:', error); | |
| } finally { | |
| hideProcessing(); | |
| } | |
| } | |
| // Randomize form | |
| async function randomizeForm() { | |
| try { | |
| showProcessing('Randomizing form...'); | |
| // Mock API call - replace with actual API endpoint | |
| const response = await fetch(`${API_BASE}/randomize`); | |
| if (!response.ok) throw new Error('Failed to randomize'); | |
| const randomData = await response.json(); | |
| // Fill form fields with random data | |
| document.getElementById('songTitle').value = randomData.songTitle; | |
| document.getElementById('occasion').value = randomData.occasion; | |
| document.getElementById('buyerName').value = randomData.buyerName; | |
| document.getElementById('recipientName').value = randomData.recipientName; | |
| document.getElementById('relationship').value = randomData.relationship; | |
| document.getElementById('tone').value = randomData.tone; | |
| document.getElementById('mainGenre').value = randomData.mainGenre; | |
| document.getElementById('secondaryGenres').value = randomData.secondaryGenres; | |
| document.getElementById('tempo').value = randomData.tempo; | |
| document.getElementById('vocalStyle').value = randomData.vocalStyle; | |
| document.getElementById('instrumentationNotes').value = randomData.instrumentationNotes; | |
| document.getElementById('keyFacts').value = randomData.keyFacts.join('\n'); | |
| document.getElementById('stories').value = randomData.stories.join('\n'); | |
| document.getElementById('mainMessage').value = randomData.mainMessage; | |
| document.getElementById('namesPlaces').value = randomData.namesPlaces; | |
| document.getElementById('visualStyle').value = randomData.visualStyle; | |
| document.getElementById('visualMood').value = randomData.visualMood; | |
| document.getElementById('visualPlacesThemes').value = randomData.visualPlacesThemes; | |
| document.getElementById('visualText').value = randomData.visualText; | |
| showStatus(warningsDisplay, 'Form randomized with API data!', 'success'); | |
| } catch (error) { | |
| showStatus(warningsDisplay, `Error randomizing: ${error.message}`, 'error'); | |
| console.error('Randomization error:', error); | |
| } finally { | |
| hideProcessing(); | |
| } | |
| } | |
| // Reset form | |
| function resetForm() { | |
| if (confirm('Are you sure you want to reset the form? All data will be lost.')) { | |
| document.querySelectorAll('input, textarea, select').forEach(el => { | |
| if (el.type === 'checkbox') { | |
| el.checked = false; | |
| } else { | |
| el.value = ''; | |
| } | |
| }); | |
| sunoBlockOut.textContent = 'Suno block will appear here...'; | |
| warningsDisplay.textContent = ''; | |
| diagOut.textContent = ''; | |
| currentOrderId = null; | |
| orderIdDisplay.textContent = 'none'; | |
| document.getElementById('songPrompt').value = ''; | |
| document.getElementById('artPrompt').value = ''; | |
| document.getElementById('videoPrompt').value = ''; | |
| document.getElementById('imgGrid').innerHTML = ''; | |
| document.getElementById('videoPreview').innerHTML = ''; | |
| document.getElementById('videoOut').textContent = 'Video status...'; | |
| showStatus(warningsDisplay, 'Form reset complete!', 'success'); | |
| } | |
| } | |
| // Save draft | |
| function saveDraft() { | |
| const draft = { | |
| formData: getFormData(), | |
| sunoBlock: sunoBlockOut.textContent, | |
| songPrompt: document.getElementById('songPrompt').value, | |
| artPrompt: document.getElementById('artPrompt').value, | |
| videoPrompt: document.getElementById('videoPrompt').value, | |
| orderId: currentOrderId | |
| }; | |
| localStorage.setItem('fiverrMusicDraft', JSON.stringify(draft)); | |
| showStatus(warningsDisplay, 'Draft saved to browser storage!', 'success'); | |
| } | |
| // Load draft | |
| function loadDraft() { | |
| const draftData = localStorage.getItem('fiverrMusicDraft'); | |
| if (!draftData) { | |
| showStatus(warningsDisplay, 'No saved draft found!', 'warning'); | |
| return; | |
| } | |
| try { | |
| const draft = JSON.parse(draftData); | |
| // Restore form data | |
| document.getElementById('songTitle').value = draft.formData.songTitle; | |
| document.getElementById('occasion').value = draft.formData.occasion; | |
| document.getElementById('buyerName').value = draft.formData.customerName; | |
| document.getElementById('recipientName').value = draft.formData.recipient_name; | |
| document.getElementById('relationship').value = draft.formData.relationship; | |
| document.getElementById('tone').value = draft.formData.tone_vibe; | |
| document.getElementById('mainGenre').value = draft.formData.main_genre; | |
| document.getElementById('secondaryGenres').value = draft.formData.secondary_influences; | |
| document.getElementById('tempo').value = draft.formData.tempo_energy; | |
| document.getElementById('vocalStyle').value = draft.formData.vocal_style; | |
| document.getElementById('instrumentationNotes').value = draft.formData.instrumentation_notes; | |
| document.getElementById('languageLevel').value = draft.formData.language_level; | |
| document.getElementById('packageLevel').value = draft.formData.package_level; | |
| document.getElementById('keyFacts').value = draft.formData.key_facts; | |
| document.getElementById('stories').value = draft.formData.stories; | |
| document.getElementById('mainMessage').value = draft.formData.main_message; | |
| document.getElementById('namesPlaces').value = draft.formData.names_places; | |
| document.getElementById('visualStyle').value = draft.formData.visual_style; | |
| document.getElementById('visualMood').value = draft.formData.visual_mood; | |
| document.getElementById('visualPlacesThemes').value = draft.formData.visual_places_themes; | |
| document.getElementById('visualText').value = draft.formData.visual_text; | |
| document.getElementById('wantsArtwork').checked = draft.formData.wants_artwork; | |
| document.getElementById('wantsVideo').checked = draft.formData.wants_video; | |
| // Restore generated content | |
| sunoBlockOut.textContent = draft.sunoBlock; | |
| document.getElementById('songPrompt').value = draft.songPrompt; | |
| document.getElementById('artPrompt').value = draft.artPrompt; | |
| document.getElementById('videoPrompt').value = draft.videoPrompt; | |
| currentOrderId = draft.orderId; | |
| orderIdDisplay.textContent = currentOrderId; | |
| showStatus(warningsDisplay, 'Draft loaded successfully!', 'success'); | |
| } catch (error) { | |
| showStatus(warningsDisplay, `Error loading draft: ${error.message}`, 'error'); | |
| console.error('Draft load error:', error); | |
| } | |
| } | |
| // Copy Suno block to clipboard | |
| function copySunoBlock() { | |
| copyToClipboard(sunoBlockOut.textContent); | |
| showStatus(warningsDisplay, 'Suno block copied to clipboard!', 'success'); | |
| } | |
| // Run diagnostics | |
| async function runDiagnostics() { | |
| try { | |
| showProcessing('Running diagnostics...'); | |
| const response = await fetch(`${API_BASE}/diagnostics`); | |
| if (!response.ok) throw new Error('Diagnostics failed'); | |
| const diagData = await response.json(); | |
| diagOut.textContent = JSON.stringify(diagData, null, 2); | |
| diagOut.style.display = 'block'; | |
| showStatus(warningsDisplay, 'Diagnostics complete!', 'success'); | |
| } catch (error) { | |
| diagOut.textContent = `Error running diagnostics: ${error.message}`; | |
| diagOut.style.display = 'block'; | |
| console.error('Diagnostics error:', error); | |
| } finally { | |
| hideProcessing(); | |
| } | |
| } | |
| // Generate image | |
| async function generateImage() { | |
| if (!apiKey) { | |
| showStatus(warningsDisplay, 'Please save your OpenAI API key first!', 'error'); | |
| return; | |
| } | |
| const prompt = document.getElementById('artPrompt').value; | |
| if (!prompt) { | |
| showStatus(warningsDisplay, 'Please generate an artwork prompt first!', 'error'); | |
| return; | |
| } | |
| try { | |
| showProcessing('Generating image...'); | |
| const response = await fetch(`${API_BASE}/generate/image`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'X-API-Key': apiKey | |
| }, | |
| body: JSON.stringify({ prompt }) | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json(); | |
| throw new Error(errorData.error || 'Image generation failed'); | |
| } | |
| const result = await response.json(); | |
| displayGeneratedImages(result.images); | |
| showStatus(warningsDisplay, 'Image generation complete!', 'success'); | |
| } catch (error) { | |
| showStatus(warningsDisplay, `Error generating image: ${error.message}`, 'error'); | |
| console.error('Image generation error:', error); | |
| } finally { | |
| hideProcessing(); | |
| } | |
| } | |
| // Display generated images | |
| function displayGeneratedImages(images) { | |
| const imgGrid = document.getElementById('imgGrid'); | |
| imgGrid.innerHTML = ''; | |
| images.forEach((imgUrl, index) => { | |
| const imgContainer = document.createElement('div'); | |
| imgContainer.className = 'thumb-container'; | |
| const img = document.createElement('img'); | |
| img.src = imgUrl; | |
| img.className = 'thumb'; | |
| img.alt = `Generated image ${index + 1}`; | |
| const downloadBtn = document.createElement('button'); | |
| downloadBtn.className = 'miniBtn'; | |
| downloadBtn.textContent = 'Download'; | |
| downloadBtn.addEventListener('click', () => { | |
| downloadImage(imgUrl, `generated-image-${index + 1}.png`); | |
| }); | |
| imgContainer.appendChild(img); | |
| imgContainer.appendChild(downloadBtn); | |
| imgGrid.appendChild(imgContainer); | |
| }); | |
| } | |
| // Generate video | |
| async function generateVideo() { | |
| if (!apiKey) { | |
| showStatus(warningsDisplay, 'Please save your OpenAI API key first!', 'error'); | |
| return; | |
| } | |
| const prompt = document.getElementById('videoPrompt').value; | |
| if (!prompt) { | |
| showStatus(warningsDisplay, 'Please generate a video prompt first!', 'error'); | |
| return; | |
| } | |
| try { | |
| showProcessing('Generating video...'); | |
| const response = await fetch(`${API_BASE}/generate/video`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'X-API-Key': apiKey | |
| }, | |
| body: JSON.stringify({ prompt }) | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json(); | |
| throw new Error(errorData.error || 'Video generation failed'); | |
| } | |
| const result = await response.json(); | |
| displayVideoPreview(result.videoUrl); | |
| showStatus(warningsDisplay, 'Video generation complete!', 'success'); | |
| } catch (error) { | |
| showStatus(warningsDisplay, `Error generating video: ${error.message}`, 'error'); | |
| console.error('Video generation error:', error); | |
| } finally { | |
| hideProcessing(); | |
| } | |
| } | |
| // Display video preview | |
| function displayVideoPreview(videoUrl) { | |
| const videoPreview = document.getElementById('videoPreview'); | |
| videoPreview.innerHTML = ''; | |
| const video = document.createElement('video'); | |
| video.src = videoUrl; | |
| video.controls = true; | |
| video.style.width = '100%'; | |
| video.style.borderRadius = '12px'; | |
| const downloadBtn = document.createElement('button'); | |
| downloadBtn.className = 'miniBtn'; | |
| downloadBtn.textContent = 'Download Video'; | |
| downloadBtn.style.marginTop = '10px'; | |
| downloadBtn.addEventListener('click', () => { | |
| downloadFile(videoUrl, 'generated-video.mp4'); | |
| }); | |
| videoPreview.appendChild(video); | |
| videoPreview.appendChild(downloadBtn); | |
| document.getElementById('videoOut').textContent = 'Video ready for download!'; | |
| } | |
| // Handle song file upload | |
| function handleSongFileUpload() { | |
| const file = songFileInput.files[0]; | |
| if (!file) return; | |
| if (!['audio/mp3', 'audio/wav', 'audio/m4a'].includes(file.type)) { | |
| showStatus(warningsDisplay, 'Please upload a valid audio file (MP3, WAV, or M4A)', 'error'); | |
| return; | |
| } | |
| songFile = file; | |
| songFileName.textContent = file.name; | |
| songFileInfo.style.display = 'flex'; | |
| showStatus(warningsDisplay, 'Song file uploaded successfully!', 'success'); | |
| } | |
| // Remove song file | |
| function removeSongFile() { | |
| songFile = null; | |
| songFileInput.value = ''; | |
| songFileInfo.style.display = 'none'; | |
| showStatus(warningsDisplay, 'Song file removed!', 'success'); | |
| } | |
| // Handle clip files upload | |
| function handleClipFilesUpload() { | |
| const files = Array.from(clipFilesInput.files); | |
| if (files.length === 0) return; | |
| const invalidFiles = files.filter(file => file.type !== 'video/mp4'); | |
| if (invalidFiles.length > 0) { | |
| showStatus(warningsDisplay, 'Only MP4 files are allowed for video clips!', 'error'); | |
| return; | |
| } | |
| videoClips = [...videoClips, ...files]; | |
| updateClipQueue(); | |
| showStatus(warnings |