Spaces:
Running
Running
| // State management | |
| let currentMode = 'template'; | |
| let isRecording = false; | |
| let isPlaying = false; | |
| let uploadedImage = null; | |
| let recordingInterval = null; | |
| let recordingTime = 0; | |
| let generationQueue = []; | |
| let currentGeneration = null; | |
| let selectedTemplate = null; | |
| let selectedMusic = null; | |
| let audioContext = null; | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', () => { | |
| lucide.createIcons(); | |
| initializeDragDrop(); | |
| initializeSliders(); | |
| initializeCheckboxes(); | |
| initializeGenerations(); | |
| }); | |
| // Drag and Drop Setup | |
| function initializeDragDrop() { | |
| const uploadZone = document.getElementById('uploadZone'); | |
| if (!uploadZone) return; | |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
| uploadZone.addEventListener(eventName, preventDefaults, false); | |
| }); | |
| function preventDefaults(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| } | |
| uploadZone.addEventListener('dragenter', () => { | |
| uploadZone.classList.add('dragover'); | |
| }); | |
| uploadZone.addEventListener('dragleave', () => { | |
| uploadZone.classList.remove('dragover'); | |
| }); | |
| uploadZone.addEventListener('drop', (e) => { | |
| uploadZone.classList.remove('dragover'); | |
| const dt = e.dataTransfer; | |
| const files = dt.files; | |
| if (files.length > 0) { | |
| handleFile(files[0]); | |
| } | |
| }); | |
| } | |
| // Initialize range sliders with value display | |
| function initializeSliders() { | |
| const sliders = document.querySelectorAll('.slider-thumb'); | |
| sliders.forEach(slider => { | |
| const updateValue = () => { | |
| const valueDisplay = slider.parentElement.querySelector('span:last-child'); | |
| if (valueDisplay) { | |
| let suffix = '%'; | |
| if (slider.parentElement.textContent.includes('Blink')) suffix = ''; | |
| valueDisplay.textContent = slider.value + suffix; | |
| } | |
| }; | |
| slider.addEventListener('input', updateValue); | |
| }); | |
| } | |
| // Initialize checkboxes with visual feedback | |
| function initializeCheckboxes() { | |
| const checkboxes = document.querySelectorAll('input[type="checkbox"]'); | |
| checkboxes.forEach(checkbox => { | |
| checkbox.addEventListener('change', (e) => { | |
| const label = e.target.closest('label'); | |
| if (label) { | |
| if (e.target.checked) { | |
| label.classList.add('bg-violet-50'); | |
| } else { | |
| label.classList.remove('bg-violet-50'); | |
| } | |
| } | |
| }); | |
| }); | |
| } | |
| // Initialize generations with demo data | |
| function initializeGenerations() { | |
| // Simulate real-time progress updates | |
| setInterval(() => { | |
| updateProcessingItems(); | |
| }, 3000); | |
| } | |
| // Mode selection | |
| function selectMode(mode, element) { | |
| currentMode = mode; | |
| // Update UI | |
| document.querySelectorAll('.mode-card').forEach(card => { | |
| card.classList.remove('active'); | |
| }); | |
| element.classList.add('active'); | |
| // Show/hide sections with animation | |
| const sections = { | |
| template: document.getElementById('templateSection'), | |
| music: document.getElementById('musicSection'), | |
| voice: document.getElementById('voiceSection') | |
| }; | |
| Object.values(sections).forEach(section => { | |
| if (section) { | |
| section.classList.add('hidden'); | |
| section.style.opacity = '0'; | |
| } | |
| }); | |
| if (sections[mode]) { | |
| sections[mode].classList.remove('hidden'); | |
| setTimeout(() => { | |
| sections[mode].style.transition = 'opacity 0.2s ease'; | |
| sections[mode].style.opacity = '1'; | |
| }, 10); | |
| } | |
| lucide.createIcons(); | |
| } | |
| // Image upload handling | |
| function handleImageUpload(event) { | |
| const file = event.target.files[0]; | |
| if (file) { | |
| handleFile(file); | |
| } | |
| } | |
| function handleFile(file) { | |
| if (!file.type.startsWith('image/')) { | |
| showNotification('Please upload an image file (JPG, PNG, WEBP)', 'error'); | |
| return; | |
| } | |
| if (file.size > 10 * 1024 * 1024) { | |
| showNotification('File size must be less than 10MB', 'error'); | |
| return; | |
| } | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| uploadedImage = e.target.result; | |
| updateUploadUI(file.name); | |
| showNotification('Image uploaded successfully!', 'success'); | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| function updateUploadUI(filename) { | |
| const placeholder = document.getElementById('uploadPlaceholder'); | |
| const preview = document.getElementById('imagePreview'); | |
| const previewImg = document.getElementById('previewImage'); | |
| const activePreview = document.getElementById('activePreview'); | |
| const previewPlaceholder = document.getElementById('previewPlaceholder'); | |
| const summaryInput = document.getElementById('summaryInput'); | |
| if (placeholder) placeholder.classList.add('hidden'); | |
| if (preview) { | |
| preview.classList.remove('hidden'); | |
| const img = preview.querySelector('img'); | |
| if (img) img.src = uploadedImage; | |
| } | |
| if (previewPlaceholder) previewPlaceholder.classList.add('hidden'); | |
| if (activePreview) { | |
| activePreview.classList.remove('hidden'); | |
| if (previewImg) previewImg.src = uploadedImage; | |
| } | |
| if (summaryInput) summaryInput.textContent = filename; | |
| } | |
| function removeImage() { | |
| uploadedImage = null; | |
| document.getElementById('uploadPlaceholder').classList.remove('hidden'); | |
| document.getElementById('imagePreview').classList.add('hidden'); | |
| document.getElementById('fileInput').value = ''; | |
| document.getElementById('previewPlaceholder').classList.remove('hidden'); | |
| document.getElementById('activePreview').classList.add('hidden'); | |
| document.getElementById('summaryInput').textContent = 'No image'; | |
| showNotification('Image removed', 'info'); | |
| } | |
| // Template selection in sidebar | |
| function selectTemplate(element, templateName) { | |
| selectedTemplate = templateName; | |
| document.querySelectorAll('.template-option').forEach(opt => { | |
| opt.classList.remove('bg-violet-50', 'border-violet-300'); | |
| opt.classList.add('border-gray-200'); | |
| }); | |
| element.classList.remove('border-gray-200'); | |
| element.classList.add('bg-violet-50', 'border-violet-300'); | |
| // Update preview if image is uploaded | |
| if (uploadedImage) { | |
| showNotification(`Template "${templateName}" selected`, 'success'); | |
| } | |
| } | |
| // Template selection in grid | |
| function selectTemplateCard(element) { | |
| document.querySelectorAll('.template-card').forEach(card => { | |
| card.classList.remove('selected'); | |
| }); | |
| element.classList.add('selected'); | |
| // Extract template name | |
| const nameEl = element.querySelector('h4'); | |
| if (nameEl) selectedTemplate = nameEl.textContent; | |
| // Switch to preview tab with animation | |
| setTimeout(() => { | |
| const previewBtn = document.querySelector('button[onclick*="preview"]'); | |
| if (previewBtn) switchTab('preview', previewBtn); | |
| }, 200); | |
| showNotification('Template selected', 'success'); | |
| } | |
| // Tab switching | |
| function switchTab(tabName, element) { | |
| const tabs = ['previewTab', 'templatesTab', 'audioTab', 'captionsTab']; | |
| tabs.forEach(tab => { | |
| const el = document.getElementById(tab); | |
| if (el) { | |
| el.classList.add('hidden'); | |
| el.style.opacity = '0'; | |
| } | |
| }); | |
| const selectedTab = document.getElementById(tabName + 'Tab'); | |
| if (selectedTab) { | |
| selectedTab.classList.remove('hidden'); | |
| setTimeout(() => { | |
| selectedTab.style.transition = 'opacity 0.3s ease'; | |
| selectedTab.style.opacity = '1'; | |
| }, 10); | |
| } | |
| document.querySelectorAll('.tab-active').forEach(btn => { | |
| btn.classList.remove('tab-active'); | |
| btn.classList.add('text-gray-500'); | |
| }); | |
| if (element) { | |
| element.classList.remove('text-gray-500'); | |
| element.classList.add('tab-active'); | |
| } | |
| lucide.createIcons(); | |
| } | |
| // Aspect ratio selection | |
| function setAspect(btn) { | |
| document.querySelectorAll('.aspect-btn').forEach(b => { | |
| b.classList.remove('border-violet-500', 'bg-violet-50', 'text-violet-700'); | |
| b.classList.add('border-gray-200', 'text-gray-600'); | |
| }); | |
| btn.classList.remove('border-gray-200', 'text-gray-600'); | |
| btn.classList.add('border-violet-500', 'bg-violet-50', 'text-violet-700'); | |
| // Update preview aspect ratio | |
| const activePreview = document.getElementById('activePreview'); | |
| if (activePreview) { | |
| const ratio = btn.textContent.trim(); | |
| if (ratio === '1:1') activePreview.style.aspectRatio = '1/1'; | |
| else if (ratio === '4:5') activePreview.style.aspectRatio = '4/5'; | |
| else if (ratio === '9:16') activePreview.style.aspectRatio = '9/16'; | |
| else if (ratio === '16:9') activePreview.style.aspectRatio = '16/9'; | |
| } | |
| } | |
| // Recording functionality | |
| function toggleRecording() { | |
| const btn = document.getElementById('recordBtn'); | |
| const icon = document.getElementById('recordIcon'); | |
| const text = document.getElementById('recordText'); | |
| if (!isRecording) { | |
| // Start recording | |
| isRecording = true; | |
| recordingTime = 0; | |
| if (icon) { | |
| icon.classList.add('recording-pulse'); | |
| icon.classList.remove('rounded-full'); | |
| icon.classList.add('rounded-sm'); | |
| } | |
| if (text) text.textContent = 'Stop Recording (0:00)'; | |
| if (btn) { | |
| btn.classList.remove('bg-red-50', 'border-red-200'); | |
| btn.classList.add('bg-red-100', 'border-red-300'); | |
| } | |
| // Simulate recording timer | |
| recordingInterval = setInterval(() => { | |
| recordingTime++; | |
| const mins = Math.floor(recordingTime / 60); | |
| const secs = recordingTime % 60; | |
| if (text) { | |
| text.textContent = `Stop Recording (${mins}:${secs.toString().padStart(2, '0')})`; | |
| } | |
| }, 1000); | |
| showNotification('Recording started...', 'info'); | |
| // Simulate recording limit | |
| setTimeout(() => { | |
| if (isRecording) toggleRecording(); | |
| }, 60000); // Auto stop after 60 seconds | |
| } else { | |
| // Stop recording | |
| isRecording = false; | |
| clearInterval(recordingInterval); | |
| if (icon) { | |
| icon.classList.remove('recording-pulse'); | |
| icon.classList.add('rounded-full'); | |
| icon.classList.remove('rounded-sm'); | |
| } | |
| if (text) text.textContent = 'Record Voice'; | |
| if (btn) { | |
| btn.classList.add('bg-red-50', 'border-red-200'); | |
| btn.classList.remove('bg-red-100', 'border-red-300'); | |
| } | |
| showNotification(`Recording saved (${recordingTime}s)`, 'success'); | |
| // Add to generations | |
| addGenerationToQueue({ | |
| name: `Voice Recording (${recordingTime}s)`, | |
| type: 'Voice', | |
| status: 'completed', | |
| duration: `${recordingTime}s`, | |
| resolution: '1080p' | |
| }); | |
| } | |
| } | |
| // Play/Pause toggle for preview | |
| function togglePlay() { | |
| const playIcon = document.getElementById('playIcon'); | |
| const progressBar = document.getElementById('progressBar'); | |
| isPlaying = !isPlaying; | |
| if (isPlaying) { | |
| if (playIcon) { | |
| playIcon.setAttribute('data-lucide', 'pause'); | |
| lucide.createIcons(); | |
| } | |
| // Simulate progress | |
| if (progressBar) { | |
| progressBar.style.width = '0%'; | |
| setTimeout(() => { | |
| progressBar.style.transition = 'width 15s linear'; | |
| progressBar.style.width = '100%'; | |
| }, 100); | |
| } | |
| showNotification('Playing preview...', 'info'); | |
| setTimeout(() => { | |
| isPlaying = false; | |
| if (playIcon) { | |
| playIcon.setAttribute('data-lucide', 'play'); | |
| lucide.createIcons(); | |
| } | |
| if (progressBar) { | |
| progressBar.style.transition = 'none'; | |
| progressBar.style.width = '0%'; | |
| } | |
| }, 15000); | |
| } else { | |
| if (playIcon) { | |
| playIcon.setAttribute('data-lucide', 'play'); | |
| lucide.createIcons(); | |
| } | |
| if (progressBar) { | |
| progressBar.style.transition = 'none'; | |
| } | |
| } | |
| } | |
| // Generate Lip Sync | |
| function generateLipSync() { | |
| if (!uploadedImage) { | |
| showNotification('Please upload a cat photo first', 'error'); | |
| document.getElementById('uploadZone').scrollIntoView({ behavior: 'smooth' }); | |
| return; | |
| } | |
| if (!selectedTemplate && currentMode === 'template') { | |
| showNotification('Please select a template', 'error'); | |
| return; | |
| } | |
| // Deduct credits | |
| const creditsBadge = document.querySelector('.text-violet-900'); | |
| if (creditsBadge) { | |
| let credits = parseInt(creditsBadge.textContent.replace(/,/g, '')); | |
| credits -= 45; | |
| creditsBadge.textContent = credits.toLocaleString() + ' credits'; | |
| } | |
| // Add to generations | |
| const generationId = Date.now(); | |
| const newGeneration = { | |
| id: generationId, | |
| name: selectedTemplate || 'Custom Generation', | |
| type: currentMode === 'template' ? 'Template' : currentMode === 'music' ? 'Music' : 'Voice', | |
| status: 'processing', | |
| progress: 0, | |
| timestamp: new Date(), | |
| duration: '0:15', | |
| resolution: '1080p' | |
| }; | |
| addGenerationToQueue(newGeneration); | |
| showNotification('Generation started! Check the Generations panel.', 'success'); | |
| // Simulate generation progress | |
| simulateGenerationProgress(generationId); | |
| } | |
| // Add generation to queue and UI | |
| function addGenerationToQueue(generation) { | |
| generationQueue.push(generation); | |
| renderGenerationCard(generation); | |
| } | |
| // Render generation card | |
| function renderGenerationCard(generation) { | |
| const container = document.querySelector('.overflow-y-auto.custom-scrollbar'); | |
| if (!container) return; | |
| const card = document.createElement('div'); | |
| card.className = 'generation-card bg-white border border-gray-200 rounded-xl p-3 shadow-sm mb-3'; | |
| card.id = `gen-${generation.id}`; | |
| const statusColor = generation.status === 'completed' ? 'green' : generation.status === 'failed' ? 'red' : 'amber'; | |
| card.innerHTML = ` | |
| <div class="flex items-start space-x-3"> | |
| <div class="w-16 h-16 bg-gray-100 rounded-lg flex-shrink-0 overflow-hidden relative"> | |
| ${uploadedImage ? `<img src="${uploadedImage}" class="w-full h-full object-cover opacity-50">` : ''} | |
| ${generation.status === 'processing' ? ` | |
| <div class="absolute inset-0 flex items-center justify-center"> | |
| <div class="w-6 h-6 border-2 border-violet-600 border-t-transparent rounded-full animate-spin"></div> | |
| </div> | |
| ` : ''} | |
| </div> | |
| <div class="flex-1 min-w-0"> | |
| <div class="flex items-center justify-between mb-1"> | |
| <span class="text-[10px] font-medium text-${statusColor}-600 bg-${statusColor}-50 px-1.5 py-0.5 rounded capitalize">${generation.status}</span> | |
| <button class="text-gray-400 hover:text-gray-600" onclick="deleteGeneration(${generation.id})"> | |
| <i data-lucide="x" class="w-3 h-3"></i> | |
| </button> | |
| </div> | |
| <p class="text-xs font-medium text-gray-900 truncate">${generation.name}</p> | |
| <p class="text-[10px] text-gray-500">${generation.type} • Just now</p> | |
| ${generation.status === 'processing' ? ` | |
| <div class="mt-2"> | |
| <div class="flex justify-between text-[10px] text-gray-500 mb-1"> | |
| <span>Rendering lip sync...</span> | |
| <span class="progress-text">0%</span> | |
| </div> | |
| <div class="h-1 bg-gray-100 rounded-full overflow-hidden"> | |
| <div class="h-full progress-bar w-0 rounded-full transition-all duration-300"></div> | |
| </div> | |
| </div> | |
| ` : ''} | |
| </div> | |
| </div> | |
| `; | |
| container.insertBefore(card, container.firstChild); | |
| lucide.createIcons(); | |
| } | |
| // Simulate generation progress | |
| function simulateGenerationProgress(generationId) { | |
| let progress = 0; | |
| const interval = setInterval(() => { | |
| progress += Math.random() * 15; | |
| if (progress >= 100) { | |
| progress = 100; | |
| clearInterval(interval); | |
| completeGeneration(generationId); | |
| } | |
| updateGenerationProgress(generationId, progress); | |
| }, 1000); | |
| } | |
| // Update generation progress UI | |
| function updateGenerationProgress(generationId, progress) { | |
| const card = document.getElementById(`gen-${generationId}`); | |
| if (!card) return; | |
| const bar = card.querySelector('.progress-bar'); | |
| const text = card.querySelector('.progress-text'); | |
| if (bar) bar.style.width = progress + '%'; | |
| if (text) text.textContent = Math.round(progress) + '%'; | |
| } | |
| // Complete generation | |
| function completeGeneration(generationId) { | |
| const card = document.getElementById(`gen-${generationId}`); | |
| if (!card) return; | |
| card.innerHTML = ` | |
| <div class="flex items-start space-x-3"> | |
| <div class="w-16 h-16 bg-gray-100 rounded-lg flex-shrink-0 overflow-hidden relative group cursor-pointer"> | |
| <img src="${uploadedImage}" class="w-full h-full object-cover"> | |
| <div class="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"> | |
| <i data-lucide="play" class="w-6 h-6 text-white fill-current"></i> | |
| </div> | |
| </div> | |
| <div class="flex-1 min-w-0"> | |
| <div class="flex items-center justify-between mb-1"> | |
| <span class="text-[10px] font-medium text-green-600 bg-green-50 px-1.5 py-0.5 rounded">Completed</span> | |
| <button class="text-gray-400 hover:text-gray-600" onclick="deleteGeneration(${generationId})"> | |
| <i data-lucide="x" class="w-3 h-3"></i> | |
| </button> | |
| </div> | |
| <p class="text-xs font-medium text-gray-900 truncate">Ready to download</p> | |
| <p class="text-[10px] text-gray-500">0:15 • 1080p</p> | |
| <div class="flex items-center space-x-2 mt-2 opacity-0 group-hover:opacity-100 transition-opacity"> | |
| <button class="text-[10px] text-violet-600 hover:text-violet-700 font-medium" onclick="downloadGeneration(${generationId})"> | |
| Download | |
| </button> | |
| <button class="text-[10px] text-gray-500 hover:text-gray-700 font-medium">Duplicate</button> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| card.classList.add('group', 'cursor-pointer'); | |
| lucide.createIcons(); | |
| showNotification('Generation complete!', 'success'); | |
| } | |
| // Delete generation | |
| function deleteGeneration(generationId) { | |
| const card = document.getElementById(`gen-${generationId}`); | |
| if (card) { | |
| card.style.opacity = '0'; | |
| card.style.transform = 'translateX(20px)'; | |
| setTimeout(() => card.remove(), 300); | |
| generationQueue = generationQueue.filter(g => g.id !== generationId); | |
| } | |
| } | |
| // Download generation | |
| function downloadGeneration(generationId) { | |
| showNotification('Downloading video...', 'success'); | |
| // Simulate download | |
| setTimeout(() => { | |
| showNotification('Download complete!', 'success'); | |
| }, 2000); | |
| } | |
| // Update processing items animation | |
| function updateProcessingItems() { | |
| const processingBars = document.querySelectorAll('.progress-bar'); | |
| processingBars.forEach(bar => { | |
| if (bar.style.width && parseInt(bar.style.width) < 100) { | |
| const currentWidth = parseInt(bar.style.width) || 0; | |
| const newWidth = Math.min(currentWidth + Math.random() * 10, 95); | |
| bar.style.width = newWidth + '%'; | |
| const text = bar.parentElement.previousElementSibling?.querySelector('.progress-text'); | |
| if (text) text.textContent = Math.round(newWidth) + '%'; | |
| } | |
| }); | |
| } | |
| // Reset form | |
| function resetForm() { | |
| if (confirm('Reset all settings? This will clear your image and selections.')) { | |
| removeImage(); | |
| document.querySelectorAll('.template-option').forEach(opt => { | |
| opt.classList.remove('bg-violet-50', 'border-violet-300'); | |
| opt.classList.add('border-gray-200'); | |
| }); | |
| document.querySelectorAll('.template-card').forEach(card => { | |
| card.classList.remove('selected'); | |
| }); | |
| document.querySelectorAll('.slider-thumb').forEach(slider => { | |
| slider.value = 50; | |
| const display = slider.parentElement.querySelector('span:last-child'); | |
| if (display) display.textContent = '50%'; | |
| }); | |
| document.querySelectorAll('input[type="checkbox"]').forEach(cb => { | |
| cb.checked = false; | |
| }); | |
| selectMode('template', document.querySelector('.mode-card.active')); | |
| document.querySelectorAll('.aspect-btn').forEach((btn, index) => { | |
| if (index === 0) setAspect(btn); | |
| }); | |
| showNotification('Form reset', 'info'); | |
| } | |
| } | |
| // Notification system | |
| function showNotification(message, type = 'info') { | |
| const notification = document.createElement('div'); | |
| const colors = { | |
| success: 'bg-green-500', | |
| error: 'bg-red-500', | |
| info: 'bg-violet-500' | |
| }; | |
| notification.className = `fixed bottom-4 right-4 ${colors[type]} text-white px-4 py-2 rounded-lg shadow-lg z-50 text-sm font-medium transform translate-y-0 transition-all duration-300`; | |
| notification.textContent = message; | |
| document.body.appendChild(notification); | |
| setTimeout(() => { | |
| notification.style.opacity = '0'; | |
| notification.style.transform = 'translateY(10px)'; | |
| setTimeout(() => notification.remove(), 300); | |
| }, 3000); | |
| } | |
| // Music selection | |
| function selectMusic(element, trackName) { | |
| selectedMusic = trackName; | |
| document.querySelectorAll('.music-track').forEach(track => { | |
| track.classList.remove('bg-pink-50', 'border-pink-300'); | |
| }); | |
| element.classList.add('bg-pink-50', 'border-pink-300'); | |
| showNotification(`Music track "${trackName}" selected`, 'success'); | |
| } | |
| // Keyboard shortcuts | |
| document.addEventListener('keydown', (e) => { | |
| // Space to play/pause when preview is visible | |
| if (e.code === 'Space' && uploadedImage && document.activePreview?.classList.contains('hidden')) { | |
| e.preventDefault(); | |
| togglePlay(); | |
| } | |
| // ESC to close/cancel | |
| if (e.code === 'Escape' && isRecording) { | |
| toggleRecording(); | |
| } | |
| // G to generate | |
| if (e.code === 'KeyG' && e.ctrlKey) { | |
| e.preventDefault(); | |
| generateLipSync(); | |
| } | |
| }); |