Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>TrendClip - Video Composer</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 20px; | |
| } | |
| .container { | |
| width: 100%; | |
| max-width: 900px; | |
| background: white; | |
| border-radius: 16px; | |
| box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); | |
| overflow: hidden; | |
| } | |
| .header { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 40px; | |
| text-align: center; | |
| } | |
| .header h1 { | |
| font-size: 2.5em; | |
| margin-bottom: 10px; | |
| font-weight: 700; | |
| } | |
| .header p { | |
| font-size: 1.1em; | |
| opacity: 0.9; | |
| } | |
| .content { | |
| padding: 40px; | |
| } | |
| .form-group { | |
| margin-bottom: 30px; | |
| } | |
| .form-group label { | |
| display: block; | |
| margin-bottom: 12px; | |
| font-weight: 600; | |
| color: #333; | |
| font-size: 1.1em; | |
| } | |
| .form-group input, | |
| .form-group textarea { | |
| width: 100%; | |
| padding: 14px 16px; | |
| border: 2px solid #e0e0e0; | |
| border-radius: 8px; | |
| font-size: 1em; | |
| font-family: inherit; | |
| transition: all 0.3s ease; | |
| } | |
| .form-group textarea { | |
| min-height: 120px; | |
| resize: vertical; | |
| } | |
| .form-group input:focus, | |
| .form-group textarea:focus { | |
| outline: none; | |
| border-color: #667eea; | |
| box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); | |
| } | |
| .button-group { | |
| display: flex; | |
| gap: 12px; | |
| margin-bottom: 30px; | |
| } | |
| button { | |
| flex: 1; | |
| padding: 14px 24px; | |
| border: none; | |
| border-radius: 8px; | |
| font-size: 1em; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| } | |
| .btn-generate { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| } | |
| .btn-generate:hover:not(:disabled) { | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3); | |
| } | |
| .btn-generate:disabled { | |
| opacity: 0.6; | |
| cursor: not-allowed; | |
| } | |
| .btn-download { | |
| background: #4caf50; | |
| color: white; | |
| display: none; | |
| } | |
| .btn-download:hover:not(:disabled) { | |
| background: #45a049; | |
| transform: translateY(-2px); | |
| } | |
| .btn-download:disabled { | |
| opacity: 0.6; | |
| cursor: not-allowed; | |
| } | |
| .status { | |
| padding: 16px; | |
| border-radius: 8px; | |
| margin-bottom: 20px; | |
| display: none; | |
| font-weight: 500; | |
| } | |
| .status.loading { | |
| background: #e3f2fd; | |
| color: #1976d2; | |
| display: block; | |
| } | |
| .status.success { | |
| background: #e8f5e9; | |
| color: #388e3c; | |
| display: block; | |
| } | |
| .status.error { | |
| background: #ffebee; | |
| color: #c62828; | |
| display: block; | |
| } | |
| .progress-bar { | |
| width: 100%; | |
| height: 4px; | |
| background: #e0e0e0; | |
| border-radius: 2px; | |
| overflow: hidden; | |
| margin-top: 8px; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); | |
| width: 0%; | |
| animation: loading 2s infinite; | |
| } | |
| @keyframes loading { | |
| 0% { width: 0%; } | |
| 50% { width: 70%; } | |
| 100% { width: 100%; } | |
| } | |
| .video-container { | |
| display: none; | |
| margin: 30px 0; | |
| } | |
| .video-container.show { | |
| display: block; | |
| } | |
| .video-wrapper { | |
| position: relative; | |
| width: 100%; | |
| background: #000; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| margin-bottom: 20px; | |
| } | |
| video { | |
| width: 100%; | |
| height: auto; | |
| display: block; | |
| } | |
| .video-info { | |
| background: #f5f5f5; | |
| padding: 16px; | |
| border-radius: 8px; | |
| margin-bottom: 16px; | |
| } | |
| .video-info p { | |
| margin: 8px 0; | |
| color: #666; | |
| font-size: 0.95em; | |
| } | |
| .video-info strong { | |
| color: #333; | |
| } | |
| .spinner { | |
| display: inline-block; | |
| width: 20px; | |
| height: 20px; | |
| border: 3px solid rgba(102, 126, 234, 0.3); | |
| border-top-color: #667eea; | |
| border-radius: 50%; | |
| animation: spin 0.8s linear infinite; | |
| vertical-align: middle; | |
| margin-right: 8px; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| .steps { | |
| display: none; | |
| margin: 20px 0; | |
| padding: 20px; | |
| background: #f9f9f9; | |
| border-radius: 8px; | |
| border-left: 4px solid #667eea; | |
| } | |
| .steps.show { | |
| display: block; | |
| } | |
| .step { | |
| display: flex; | |
| align-items: center; | |
| margin: 10px 0; | |
| font-size: 0.9em; | |
| } | |
| .step-number { | |
| display: inline-block; | |
| width: 24px; | |
| height: 24px; | |
| background: #667eea; | |
| color: white; | |
| border-radius: 50%; | |
| text-align: center; | |
| line-height: 24px; | |
| margin-right: 12px; | |
| flex-shrink: 0; | |
| font-weight: 600; | |
| } | |
| .step.completed .step-number { | |
| background: #4caf50; | |
| } | |
| .step-text { | |
| color: #666; | |
| } | |
| .step.completed .step-text { | |
| color: #333; | |
| font-weight: 500; | |
| } | |
| @media (max-width: 600px) { | |
| .header h1 { | |
| font-size: 1.8em; | |
| } | |
| .content { | |
| padding: 20px; | |
| } | |
| button { | |
| padding: 12px 20px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>π¬ TrendClip</h1> | |
| <p>AI-Powered Video Composer</p> | |
| </div> | |
| <div class="content"> | |
| <form id="promptForm"> | |
| <div class="form-group"> | |
| <label for="prompt">π Describe Your Video</label> | |
| <textarea | |
| id="prompt" | |
| placeholder="e.g., signs you're highly ambitious, morning routine aesthetic, productivity tips for entrepreneurs..." | |
| required | |
| ></textarea> | |
| </div> | |
| <div class="button-group"> | |
| <button type="submit" class="btn-generate" id="generateBtn"> | |
| Generate Video | |
| </button> | |
| <button type="button" class="btn-download" id="downloadBtn" style="display: none;"> | |
| β¬οΈ Download Video | |
| </button> | |
| </div> | |
| </form> | |
| <div class="status loading" id="status"></div> | |
| <div class="steps" id="steps"> | |
| <div class="step" id="step1"> | |
| <span class="step-number">1</span> | |
| <span class="step-text">Generating manifest...</span> | |
| </div> | |
| <div class="step" id="step2"> | |
| <span class="step-number">2</span> | |
| <span class="step-text">Downloading images...</span> | |
| </div> | |
| <div class="step" id="step3"> | |
| <span class="step-number">3</span> | |
| <span class="step-text">Selecting best images...</span> | |
| </div> | |
| <div class="step" id="step4"> | |
| <span class="step-number">4</span> | |
| <span class="step-text">Composing video...</span> | |
| </div> | |
| <div class="step" id="step5"> | |
| <span class="step-number">5</span> | |
| <span class="step-text">Uploading to dataset...</span> | |
| </div> | |
| </div> | |
| <div class="video-container" id="videoContainer"> | |
| <div class="video-wrapper"> | |
| <video id="videoPlayer" controls></video> | |
| </div> | |
| <div class="video-info" id="videoInfo"> | |
| <p><strong>Status:</strong> <span id="videoStatus">Ready</span></p> | |
| <p><strong>Size:</strong> <span id="videoSize">-</span></p> | |
| <p><strong>Duration:</strong> <span id="videoDuration">-</span></p> | |
| <div id="uploadStatus" style="margin-top: 16px; padding-top: 16px; border-top: 1px solid #ddd;"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const promptForm = document.getElementById('promptForm'); | |
| const promptInput = document.getElementById('prompt'); | |
| const generateBtn = document.getElementById('generateBtn'); | |
| const downloadBtn = document.getElementById('downloadBtn'); | |
| const statusDiv = document.getElementById('status'); | |
| const stepsDiv = document.getElementById('steps'); | |
| const videoContainer = document.getElementById('videoContainer'); | |
| const videoPlayer = document.getElementById('videoPlayer'); | |
| const videoStatus = document.getElementById('videoStatus'); | |
| const videoSize = document.getElementById('videoSize'); | |
| const videoDuration = document.getElementById('videoDuration'); | |
| let videoBlob = null; | |
| promptForm.addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| await generateVideo(); | |
| }); | |
| async function generateVideo() { | |
| const prompt = promptInput.value.trim(); | |
| if (!prompt) { | |
| showStatus('Please enter a prompt', 'error'); | |
| return; | |
| } | |
| // Reset UI | |
| videoContainer.classList.remove('show'); | |
| downloadBtn.style.display = 'none'; | |
| generateBtn.disabled = true; | |
| stepsDiv.classList.add('show'); | |
| updateAllSteps(''); | |
| showStatus( | |
| `<span class="spinner"></span>Generating your video...`, | |
| 'loading' | |
| ); | |
| try { | |
| const response = await fetch('/generate-from-prompt', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ prompt }) | |
| }); | |
| if (!response.ok) { | |
| const error = await response.json(); | |
| throw new Error(error.detail || 'Video generation failed'); | |
| } | |
| // Mark steps as completed | |
| updateSteps([1, 2, 3, 4, 5]); | |
| // Get video blob | |
| videoBlob = await response.blob(); | |
| // Read upload status from response headers | |
| const uploadStatus = response.headers.get('X-Upload-Status') || 'unknown'; | |
| const uploadMessage = response.headers.get('X-Upload-Message') || ''; | |
| const datasetPath = response.headers.get('X-Dataset-Path') || ''; | |
| // Display video | |
| const videoUrl = URL.createObjectURL(videoBlob); | |
| videoPlayer.src = videoUrl; | |
| videoContainer.classList.add('show'); | |
| downloadBtn.style.display = 'block'; | |
| // Update video info | |
| const sizeMB = (videoBlob.size / (1024 * 1024)).toFixed(2); | |
| videoSize.textContent = `${sizeMB} MB`; | |
| videoStatus.textContent = 'Ready to download'; | |
| // Display upload status | |
| const uploadStatusDiv = document.getElementById('uploadStatus'); | |
| if (uploadStatus === 'success') { | |
| uploadStatusDiv.innerHTML = ` | |
| <p style="color: #388e3c; font-weight: 500;"> | |
| β Uploaded to Dataset Successfully! | |
| </p> | |
| <p style="color: #666; font-size: 0.9em;"> | |
| π ${datasetPath} | |
| </p> | |
| `; | |
| } else if (uploadStatus === 'warning') { | |
| uploadStatusDiv.innerHTML = ` | |
| <p style="color: #f57f17; font-weight: 500;"> | |
| β οΈ ${uploadMessage} | |
| </p> | |
| `; | |
| } else if (uploadStatus === 'error') { | |
| uploadStatusDiv.innerHTML = ` | |
| <p style="color: #c62828; font-weight: 500;"> | |
| β Upload Error | |
| </p> | |
| <p style="color: #666; font-size: 0.9em;"> | |
| ${uploadMessage} | |
| </p> | |
| `; | |
| } | |
| showStatus('β Video generated successfully!', 'success'); | |
| } catch (error) { | |
| showStatus(`β Error: ${error.message}`, 'error'); | |
| updateAllSteps(''); | |
| } finally { | |
| generateBtn.disabled = false; | |
| } | |
| } | |
| function showStatus(message, type) { | |
| statusDiv.innerHTML = message; | |
| statusDiv.className = `status ${type}`; | |
| } | |
| function updateSteps(completedSteps) { | |
| for (let i = 1; i <= 5; i++) { | |
| const step = document.getElementById(`step${i}`); | |
| if (completedSteps.includes(i)) { | |
| step.classList.add('completed'); | |
| } else { | |
| step.classList.remove('completed'); | |
| } | |
| } | |
| } | |
| function updateAllSteps(type) { | |
| for (let i = 1; i <= 4; i++) { | |
| const step = document.getElementById(`step${i}`); | |
| step.classList.remove('completed'); | |
| } | |
| } | |
| downloadBtn.addEventListener('click', () => { | |
| if (!videoBlob) return; | |
| const url = URL.createObjectURL(videoBlob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `trendclip_${Date.now()}.mp4`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| }); | |
| // Set initial focus | |
| promptInput.focus(); | |
| </script> | |
| </body> | |
| </html> | |