|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>AI-Powered Slideshow Creator</title> |
|
<script src="https://cdn.tailwindcss.com"></script> |
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
|
<style> |
|
.dropzone { |
|
border: 2px dashed #9CA3AF; |
|
border-radius: 0.5rem; |
|
transition: all 0.3s ease; |
|
} |
|
.dropzone.active { |
|
border-color: #3B82F6; |
|
background-color: #EFF6FF; |
|
} |
|
.preview-image { |
|
transition: all 0.3s ease; |
|
} |
|
.preview-image:hover { |
|
transform: scale(1.03); |
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); |
|
} |
|
.progress-bar { |
|
transition: width 0.3s ease; |
|
} |
|
#videoPreview { |
|
max-width: 100%; |
|
border-radius: 0.5rem; |
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); |
|
} |
|
.chat-message { |
|
max-width: 80%; |
|
border-radius: 1rem; |
|
padding: 0.75rem 1rem; |
|
margin-bottom: 0.5rem; |
|
} |
|
.user-message { |
|
background-color: #3B82F6; |
|
color: white; |
|
margin-left: auto; |
|
border-bottom-right-radius: 0.25rem; |
|
} |
|
.ai-message { |
|
background-color: #E5E7EB; |
|
color: #1F2937; |
|
margin-right: auto; |
|
border-bottom-left-radius: 0.25rem; |
|
} |
|
.ai-processing { |
|
animation: pulse 2s infinite; |
|
} |
|
@keyframes pulse { |
|
0% { opacity: 0.6; } |
|
50% { opacity: 1; } |
|
100% { opacity: 0.6; } |
|
} |
|
.error-message { |
|
background-color: #FEE2E2; |
|
color: #B91C1C; |
|
border: 1px solid #FECACA; |
|
padding: 0.75rem 1rem; |
|
border-radius: 0.5rem; |
|
margin-bottom: 1rem; |
|
} |
|
#slideshowCanvas { |
|
display: none; |
|
} |
|
</style> |
|
</head> |
|
<body class="bg-gray-50 min-h-screen"> |
|
<div class="container mx-auto px-4 py-8 max-w-6xl"> |
|
<header class="text-center mb-10"> |
|
<h1 class="text-4xl font-bold text-gray-800 mb-2">AI Slideshow Creator</h1> |
|
<p class="text-gray-600">Upload up to 100 images and let AI create a stunning slideshow</p> |
|
</header> |
|
|
|
<div class="bg-white rounded-xl shadow-lg p-6 mb-8"> |
|
<div id="errorContainer" class="hidden"></div> |
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> |
|
|
|
<div class="lg:col-span-2"> |
|
<div class="flex justify-between items-center mb-4"> |
|
<h2 class="text-2xl font-semibold text-gray-800">Upload Images</h2> |
|
<div class="text-sm text-gray-500"> |
|
<span id="imageCount">0</span>/100 images |
|
</div> |
|
</div> |
|
|
|
<div id="dropzone" class="dropzone p-8 text-center cursor-pointer mb-6"> |
|
<div class="flex flex-col items-center justify-center space-y-3"> |
|
<i class="fas fa-cloud-upload-alt text-4xl text-blue-500"></i> |
|
<p class="text-gray-600">Drag & drop your images here</p> |
|
<p class="text-sm text-gray-500">or</p> |
|
<label for="fileInput" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg cursor-pointer transition"> |
|
Browse Files (Max 100) |
|
</label> |
|
<input type="file" id="fileInput" class="hidden" multiple accept="image/*"> |
|
</div> |
|
</div> |
|
|
|
<div class="grid grid-cols-2 md:grid-cols-3 gap-4 mb-6"> |
|
<div> |
|
<label for="duration" class="block text-gray-700 mb-2">Slide Duration (sec):</label> |
|
<input type="number" id="duration" min="1" max="10" value="3" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"> |
|
</div> |
|
<div> |
|
<label for="transition" class="block text-gray-700 mb-2">Transition:</label> |
|
<select id="transition" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"> |
|
<option value="fade">Fade</option> |
|
<option value="slide">Slide</option> |
|
<option value="none">None</option> |
|
</select> |
|
</div> |
|
<div> |
|
<label for="style" class="block text-gray-700 mb-2">AI Style:</label> |
|
<select id="style" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"> |
|
<option value="auto">Auto Enhance</option> |
|
<option value="vintage">Vintage</option> |
|
<option value="modern">Modern</option> |
|
<option value="cinematic">Cinematic</option> |
|
</select> |
|
</div> |
|
</div> |
|
|
|
<div class="mb-6"> |
|
<label class="block text-gray-700 mb-2">AI Processing Options:</label> |
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-2"> |
|
<label class="flex items-center space-x-2"> |
|
<input type="checkbox" id="autoCrop" class="rounded text-blue-500" checked> |
|
<span>Auto Crop</span> |
|
</label> |
|
<label class="flex items-center space-x-2"> |
|
<input type="checkbox" id="colorCorrect" class="rounded text-blue-500" checked> |
|
<span>Color Correct</span> |
|
</label> |
|
<label class="flex items-center space-x-2"> |
|
<input type="checkbox" id="faceEnhance" class="rounded text-blue-500"> |
|
<span>Face Enhance</span> |
|
</label> |
|
<label class="flex items-center space-x-2"> |
|
<input type="checkbox" id="randomOrder" class="rounded text-blue-500"> |
|
<span>Random Order</span> |
|
</label> |
|
</div> |
|
</div> |
|
|
|
<div class="mb-6"> |
|
<label class="block text-gray-700 mb-2">Video Options:</label> |
|
<div class="grid grid-cols-2 md:grid-cols-3 gap-4"> |
|
<div> |
|
<label for="videoSize" class="block text-gray-700 mb-2">Video Size:</label> |
|
<select id="videoSize" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"> |
|
<option value="full">Full Slideshow</option> |
|
<option value="short">Short Segment (10 images)</option> |
|
<option value="medium">Medium Segment (30 images)</option> |
|
</select> |
|
</div> |
|
<div> |
|
<label for="videoQuality" class="block text-gray-700 mb-2">Quality:</label> |
|
<select id="videoQuality" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"> |
|
<option value="high">High (1080p)</option> |
|
<option value="medium">Medium (720p)</option> |
|
<option value="low">Low (480p)</option> |
|
</select> |
|
</div> |
|
<div> |
|
<label for="videoFormat" class="block text-gray-700 mb-2">Format:</label> |
|
<select id="videoFormat" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"> |
|
<option value="mp4">MP4 (Recommended)</option> |
|
<option value="webm">WebM</option> |
|
</select> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<button id="createVideoBtn" class="w-full bg-blue-500 hover:bg-blue-600 text-white py-3 px-4 rounded-lg font-medium transition flex items-center justify-center space-x-2"> |
|
<i class="fas fa-film"></i> |
|
<span>Create AI Slideshow</span> |
|
</button> |
|
</div> |
|
|
|
|
|
<div class="bg-gray-50 rounded-xl p-4"> |
|
<h2 class="text-2xl font-semibold text-gray-800 mb-4">AI Assistant</h2> |
|
|
|
<div id="chatContainer" class="h-64 overflow-y-auto mb-4 space-y-2 p-2"> |
|
<div class="chat-message ai-message"> |
|
Hi! I'm your AI assistant. I can help you create the perfect slideshow. Upload your images and tell me what you're looking for! |
|
</div> |
|
</div> |
|
|
|
<div class="flex space-x-2"> |
|
<input type="text" id="chatInput" placeholder="Ask me anything about your slideshow..." class="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"> |
|
<button id="sendChatBtn" class="bg-blue-500 hover:bg-blue-600 text-white p-2 rounded-lg"> |
|
<i class="fas fa-paper-plane"></i> |
|
</button> |
|
</div> |
|
|
|
<div class="mt-4 text-sm text-gray-600"> |
|
<p class="font-medium">Quick suggestions:</p> |
|
<div class="flex flex-wrap gap-2 mt-2"> |
|
<button class="quick-prompt bg-gray-200 hover:bg-gray-300 px-3 py-1 rounded-full text-xs">Make it romantic</button> |
|
<button class="quick-prompt bg-gray-200 hover:bg-gray-300 px-3 py-1 rounded-full text-xs">Best transitions?</button> |
|
<button class="quick-prompt bg-gray-200 hover:bg-gray-300 px-3 py-1 rounded-full text-xs">Fix dark photos</button> |
|
<button class="quick-prompt bg-gray-200 hover:bg-gray-300 px-3 py-1 rounded-full text-xs">Create short clip</button> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="mt-8"> |
|
<div class="flex justify-between items-center mb-4"> |
|
<h2 class="text-xl font-semibold text-gray-800">Your Images</h2> |
|
<button id="clearAllBtn" class="text-red-500 hover:text-red-700 text-sm flex items-center"> |
|
<i class="fas fa-trash mr-1"></i> Clear All |
|
</button> |
|
</div> |
|
|
|
<div id="imagePreviews" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3 mb-4 max-h-96 overflow-y-auto p-3 bg-gray-50 rounded-lg"> |
|
<p class="text-gray-500 col-span-full text-center py-10">No images uploaded yet</p> |
|
</div> |
|
</div> |
|
|
|
|
|
<div id="progressContainer" class="hidden mb-6 bg-blue-50 p-4 rounded-lg"> |
|
<div class="flex items-center mb-2"> |
|
<div class="bg-blue-100 p-2 rounded-full mr-3"> |
|
<i class="fas fa-cog fa-spin text-blue-500"></i> |
|
</div> |
|
<div> |
|
<h3 class="font-medium text-gray-800">AI is processing your slideshow</h3> |
|
<p id="aiStatus" class="text-sm text-gray-600">Initializing AI models...</p> |
|
</div> |
|
</div> |
|
<div class="w-full bg-gray-200 rounded-full h-2.5 mt-2"> |
|
<div id="progressBar" class="progress-bar bg-blue-600 h-2.5 rounded-full" style="width: 0%"></div> |
|
</div> |
|
<div class="flex justify-between mt-1"> |
|
<span id="progressPercent" class="text-sm font-medium text-gray-700">0%</span> |
|
<span id="progressText" class="text-sm text-gray-500">Step 1 of 10</span> |
|
</div> |
|
</div> |
|
|
|
<div id="videoContainer" class="hidden bg-white rounded-xl shadow-lg p-6 mt-8"> |
|
<div class="flex justify-between items-center mb-4"> |
|
<h2 class="text-2xl font-semibold text-gray-800">Your AI-Generated Slideshow</h2> |
|
<div class="flex space-x-2"> |
|
<button id="regenerateBtn" class="bg-gray-200 hover:bg-gray-300 text-gray-800 py-1 px-3 rounded-lg text-sm flex items-center"> |
|
<i class="fas fa-sync-alt mr-1"></i> Regenerate |
|
</button> |
|
<button id="downloadBtn" class="bg-green-500 hover:bg-green-600 text-white py-1 px-3 rounded-lg text-sm flex items-center"> |
|
<i class="fas fa-download mr-1"></i> Download |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<video id="videoPreview" controls class="w-full mb-4 rounded-lg"> |
|
Your browser does not support the video tag. |
|
</video> |
|
|
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm"> |
|
<div class="bg-gray-50 p-3 rounded-lg"> |
|
<div class="text-gray-500">Duration</div> |
|
<div id="videoDuration" class="font-medium">0:00</div> |
|
</div> |
|
<div class="bg-gray-50 p-3 rounded-lg"> |
|
<div class="text-gray-500">Images</div> |
|
<div id="videoImageCount" class="font-medium">0</div> |
|
</div> |
|
<div class="bg-gray-50 p-3 rounded-lg"> |
|
<div class="text-gray-500">Style</div> |
|
<div id="videoStyle" class="font-medium">-</div> |
|
</div> |
|
<div class="bg-gray-50 p-3 rounded-lg"> |
|
<div class="text-gray-500">Transition</div> |
|
<div id="videoTransition" class="font-medium">-</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="bg-white rounded-xl shadow-lg p-6 mt-8"> |
|
<h2 class="text-2xl font-semibold text-gray-800 mb-4">AI-Powered Features</h2> |
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6"> |
|
<div class="bg-gradient-to-br from-blue-50 to-purple-50 p-4 rounded-lg"> |
|
<div class="flex items-center mb-2"> |
|
<div class="bg-blue-100 p-2 rounded-full mr-3"> |
|
<i class="fas fa-random text-blue-500"></i> |
|
</div> |
|
<h3 class="font-medium text-gray-800">Smart Transitions</h3> |
|
</div> |
|
<p class="text-gray-600">Our AI analyzes your images and selects the perfect transitions between them for a professional look.</p> |
|
</div> |
|
<div class="bg-gradient-to-br from-purple-50 to-pink-50 p-4 rounded-lg"> |
|
<div class="flex items-center mb-2"> |
|
<div class="bg-purple-100 p-2 rounded-full mr-3"> |
|
<i class="fas fa-magic text-purple-500"></i> |
|
</div> |
|
<h3 class="font-medium text-gray-800">Image Enhancement</h3> |
|
</div> |
|
<p class="text-gray-600">Automatic color correction, cropping, and quality improvements for all your images.</p> |
|
</div> |
|
<div class="bg-gradient-to-br from-pink-50 to-red-50 p-4 rounded-lg"> |
|
<div class="flex items-center mb-2"> |
|
<div class="bg-pink-100 p-2 rounded-full mr-3"> |
|
<i class="fas fa-robot text-pink-500"></i> |
|
</div> |
|
<h3 class="font-medium text-gray-800">AI Assistant</h3> |
|
</div> |
|
<p class="text-gray-600">Get personalized recommendations and adjust settings through natural conversation.</p> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<canvas id="slideshowCanvas" style="display: none;"></canvas> |
|
|
|
<script> |
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
|
const dropzone = document.getElementById('dropzone'); |
|
const fileInput = document.getElementById('fileInput'); |
|
const imagePreviews = document.getElementById('imagePreviews'); |
|
const createVideoBtn = document.getElementById('createVideoBtn'); |
|
const progressContainer = document.getElementById('progressContainer'); |
|
const progressBar = document.getElementById('progressBar'); |
|
const progressPercent = document.getElementById('progressPercent'); |
|
const progressText = document.getElementById('progressText'); |
|
const aiStatus = document.getElementById('aiStatus'); |
|
const videoContainer = document.getElementById('videoContainer'); |
|
const videoPreview = document.getElementById('videoPreview'); |
|
const downloadBtn = document.getElementById('downloadBtn'); |
|
const regenerateBtn = document.getElementById('regenerateBtn'); |
|
const durationInput = document.getElementById('duration'); |
|
const transitionSelect = document.getElementById('transition'); |
|
const styleSelect = document.getElementById('style'); |
|
const clearAllBtn = document.getElementById('clearAllBtn'); |
|
const imageCount = document.getElementById('imageCount'); |
|
const videoDuration = document.getElementById('videoDuration'); |
|
const videoImageCount = document.getElementById('videoImageCount'); |
|
const videoStyle = document.getElementById('videoStyle'); |
|
const videoTransition = document.getElementById('videoTransition'); |
|
const chatContainer = document.getElementById('chatContainer'); |
|
const chatInput = document.getElementById('chatInput'); |
|
const sendChatBtn = document.getElementById('sendChatBtn'); |
|
const quickPrompts = document.querySelectorAll('.quick-prompt'); |
|
const errorContainer = document.getElementById('errorContainer'); |
|
const videoSize = document.getElementById('videoSize'); |
|
const videoQuality = document.getElementById('videoQuality'); |
|
const videoFormat = document.getElementById('videoFormat'); |
|
const slideshowCanvas = document.getElementById('slideshowCanvas'); |
|
const ctx = slideshowCanvas.getContext('2d'); |
|
|
|
|
|
let uploadedImages = []; |
|
let videoBlob = null; |
|
let mediaRecorder = null; |
|
let recordedChunks = []; |
|
|
|
|
|
function showError(message) { |
|
errorContainer.innerHTML = ` |
|
<div class="error-message flex items-start"> |
|
<div class="mr-2 mt-0.5"> |
|
<i class="fas fa-exclamation-circle"></i> |
|
</div> |
|
<div>${message}</div> |
|
</div> |
|
`; |
|
errorContainer.classList.remove('hidden'); |
|
|
|
|
|
setTimeout(() => { |
|
errorContainer.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); |
|
}, 100); |
|
} |
|
|
|
|
|
function clearError() { |
|
errorContainer.innerHTML = ''; |
|
errorContainer.classList.add('hidden'); |
|
} |
|
|
|
|
|
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { |
|
dropzone.addEventListener(eventName, preventDefaults, false); |
|
}); |
|
|
|
function preventDefaults(e) { |
|
e.preventDefault(); |
|
e.stopPropagation(); |
|
} |
|
|
|
['dragenter', 'dragover'].forEach(eventName => { |
|
dropzone.addEventListener(eventName, highlight, false); |
|
}); |
|
|
|
['dragleave', 'drop'].forEach(eventName => { |
|
dropzone.addEventListener(eventName, unhighlight, false); |
|
}); |
|
|
|
function highlight() { |
|
dropzone.classList.add('active'); |
|
} |
|
|
|
function unhighlight() { |
|
dropzone.classList.remove('active'); |
|
} |
|
|
|
dropzone.addEventListener('drop', handleDrop, false); |
|
fileInput.addEventListener('change', function() { |
|
handleFiles(this.files); |
|
}); |
|
|
|
function handleDrop(e) { |
|
const dt = e.dataTransfer; |
|
handleFiles(dt.files); |
|
} |
|
|
|
|
|
function handleFiles(files) { |
|
if (files.length === 0) return; |
|
|
|
|
|
if (uploadedImages.length + files.length > 100) { |
|
showError('You can upload a maximum of 100 images. Please remove some images first.'); |
|
return; |
|
} |
|
|
|
clearError(); |
|
|
|
for (let i = 0; i < files.length; i++) { |
|
const file = files[i]; |
|
if (!file.type.match('image.*')) continue; |
|
|
|
uploadedImages.push(file); |
|
} |
|
|
|
updatePreviews(); |
|
updateImageCount(); |
|
|
|
|
|
setTimeout(() => { |
|
imagePreviews.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); |
|
}, 300); |
|
} |
|
|
|
|
|
function updatePreviews() { |
|
imagePreviews.innerHTML = ''; |
|
|
|
if (uploadedImages.length === 0) { |
|
imagePreviews.innerHTML = '<p class="text-gray-500 col-span-full text-center py-10">No images uploaded yet</p>'; |
|
return; |
|
} |
|
|
|
|
|
const showCount = Math.min(uploadedImages.length, 20); |
|
|
|
for (let i = 0; i < showCount; i++) { |
|
const file = uploadedImages[i]; |
|
const reader = new FileReader(); |
|
|
|
reader.onload = function(e) { |
|
const preview = document.createElement('div'); |
|
preview.className = 'preview-image relative group'; |
|
preview.innerHTML = ` |
|
<img src="${e.target.result}" class="w-full h-24 object-cover rounded-lg"> |
|
<div class="absolute inset-0 bg-black bg-opacity-50 rounded-lg opacity-0 group-hover:opacity-100 transition flex items-center justify-center"> |
|
<button class="delete-btn bg-red-500 text-white p-1 rounded-full" data-index="${i}"> |
|
<i class="fas fa-times text-xs"></i> |
|
</button> |
|
</div> |
|
`; |
|
imagePreviews.appendChild(preview); |
|
|
|
|
|
const deleteBtn = preview.querySelector('.delete-btn'); |
|
deleteBtn.addEventListener('click', function(e) { |
|
e.stopPropagation(); |
|
const index = parseInt(this.getAttribute('data-index')); |
|
uploadedImages.splice(index, 1); |
|
updatePreviews(); |
|
updateImageCount(); |
|
}); |
|
}; |
|
reader.readAsDataURL(file); |
|
} |
|
|
|
|
|
if (uploadedImages.length > 20) { |
|
const moreCount = uploadedImages.length - 20; |
|
const morePreview = document.createElement('div'); |
|
morePreview.className = 'bg-gray-100 rounded-lg flex items-center justify-center'; |
|
morePreview.innerHTML = ` |
|
<div class="text-center p-4"> |
|
<div class="text-2xl font-bold text-gray-500">+${moreCount}</div> |
|
<div class="text-xs text-gray-500">more images</div> |
|
</div> |
|
`; |
|
imagePreviews.appendChild(morePreview); |
|
} |
|
} |
|
|
|
|
|
function updateImageCount() { |
|
imageCount.textContent = uploadedImages.length; |
|
} |
|
|
|
|
|
clearAllBtn.addEventListener('click', function() { |
|
if (uploadedImages.length === 0) return; |
|
|
|
if (confirm('Are you sure you want to remove all images?')) { |
|
uploadedImages = []; |
|
updatePreviews(); |
|
updateImageCount(); |
|
clearError(); |
|
} |
|
}); |
|
|
|
|
|
createVideoBtn.addEventListener('click', async function() { |
|
if (uploadedImages.length === 0) { |
|
showError('Please upload some images first so I can create your slideshow.'); |
|
addAIMessage("Please upload some images first so I can create your slideshow."); |
|
return; |
|
} |
|
|
|
clearError(); |
|
|
|
const duration = parseInt(durationInput.value) * 1000; |
|
const transition = transitionSelect.value; |
|
const style = styleSelect.value; |
|
const autoCrop = document.getElementById('autoCrop').checked; |
|
const colorCorrect = document.getElementById('colorCorrect').checked; |
|
const faceEnhance = document.getElementById('faceEnhance').checked; |
|
const randomOrder = document.getElementById('randomOrder').checked; |
|
const sizeOption = videoSize.value; |
|
const qualityOption = videoQuality.value; |
|
const formatOption = videoFormat.value; |
|
|
|
|
|
let imagesToUse = uploadedImages; |
|
let segmentSize = uploadedImages.length; |
|
|
|
if (sizeOption === 'short') { |
|
segmentSize = Math.min(10, uploadedImages.length); |
|
imagesToUse = uploadedImages.slice(0, segmentSize); |
|
} else if (sizeOption === 'medium') { |
|
segmentSize = Math.min(30, uploadedImages.length); |
|
imagesToUse = uploadedImages.slice(0, segmentSize); |
|
} |
|
|
|
|
|
progressContainer.classList.remove('hidden'); |
|
progressBar.style.width = '0%'; |
|
progressPercent.textContent = '0%'; |
|
|
|
|
|
createVideoBtn.disabled = true; |
|
createVideoBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i><span>AI Processing...</span>'; |
|
|
|
|
|
const aiMessages = [ |
|
"Analyzing your images...", |
|
"Enhancing photo quality...", |
|
"Adjusting colors and contrast...", |
|
"Preparing transitions...", |
|
"Arranging images for best flow...", |
|
"Applying selected style...", |
|
"Optimizing video quality...", |
|
"Finalizing video composition...", |
|
"Almost done...", |
|
"Preparing your masterpiece..." |
|
]; |
|
|
|
try { |
|
|
|
await simulateAIProcessing(aiMessages, duration, transition, style, imagesToUse.length); |
|
|
|
|
|
await createSlideshow(imagesToUse, duration, transition, formatOption); |
|
|
|
|
|
const minutes = Math.floor((duration * imagesToUse.length) / 60000); |
|
const seconds = Math.floor(((duration * imagesToUse.length) % 60000) / 1000); |
|
videoDuration.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; |
|
videoImageCount.textContent = imagesToUse.length; |
|
videoStyle.textContent = style.charAt(0).toUpperCase() + style.slice(1); |
|
videoTransition.textContent = transition.charAt(0).toUpperCase() + transition.slice(1); |
|
|
|
|
|
setTimeout(() => { |
|
videoContainer.scrollIntoView({ behavior: 'smooth' }); |
|
}, 500); |
|
|
|
addAIMessage(`Your ${sizeOption === 'full' ? 'full' : sizeOption} slideshow is ready! I've used ${transition} transitions and applied the ${style} style. You can download it or ask me to regenerate it with different settings.`); |
|
|
|
} catch (error) { |
|
console.error('Error creating video:', error); |
|
showError("Oops! Something went wrong while creating your slideshow. Please try again."); |
|
addAIMessage("Oops! Something went wrong while creating your slideshow. Please try again."); |
|
} finally { |
|
createVideoBtn.disabled = false; |
|
createVideoBtn.innerHTML = '<i class="fas fa-film"></i><span>Create AI Slideshow</span>'; |
|
} |
|
}); |
|
|
|
|
|
async function createSlideshow(images, duration, transition, format) { |
|
return new Promise(async (resolve, reject) => { |
|
try { |
|
|
|
let width, height; |
|
switch(videoQuality.value) { |
|
case 'high': |
|
width = 1920; |
|
height = 1080; |
|
break; |
|
case 'medium': |
|
width = 1280; |
|
height = 720; |
|
break; |
|
case 'low': |
|
width = 854; |
|
height = 480; |
|
break; |
|
default: |
|
width = 1280; |
|
height = 720; |
|
} |
|
|
|
slideshowCanvas.width = width; |
|
slideshowCanvas.height = height; |
|
|
|
|
|
const stream = slideshowCanvas.captureStream(); |
|
recordedChunks = []; |
|
|
|
|
|
mediaRecorder = new MediaRecorder(stream, { |
|
mimeType: format === 'webm' ? 'video/webm' : 'video/mp4' |
|
}); |
|
|
|
mediaRecorder.ondataavailable = function(e) { |
|
if (e.data.size > 0) { |
|
recordedChunks.push(e.data); |
|
} |
|
}; |
|
|
|
mediaRecorder.onstop = function() { |
|
videoBlob = new Blob(recordedChunks, { |
|
type: format === 'webm' ? 'video/webm' : 'video/mp4' |
|
}); |
|
|
|
const videoURL = URL.createObjectURL(videoBlob); |
|
videoPreview.src = videoURL; |
|
videoContainer.classList.remove('hidden'); |
|
resolve(); |
|
}; |
|
|
|
mediaRecorder.start(); |
|
|
|
|
|
for (let i = 0; i < images.length; i++) { |
|
const img = await loadImage(images[i]); |
|
|
|
|
|
drawImageToCanvas(img); |
|
|
|
|
|
if (i > 0 && transition !== 'none') { |
|
await applyTransition(transition, duration); |
|
} |
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, duration)); |
|
} |
|
|
|
mediaRecorder.stop(); |
|
|
|
} catch (error) { |
|
reject(error); |
|
} |
|
}); |
|
} |
|
|
|
|
|
function loadImage(file) { |
|
return new Promise((resolve, reject) => { |
|
const img = new Image(); |
|
img.onload = () => resolve(img); |
|
img.onerror = reject; |
|
img.src = URL.createObjectURL(file); |
|
}); |
|
} |
|
|
|
|
|
function drawImageToCanvas(img) { |
|
const canvasAspect = slideshowCanvas.width / slideshowCanvas.height; |
|
const imgAspect = img.width / img.height; |
|
|
|
let drawWidth, drawHeight, offsetX, offsetY; |
|
|
|
if (imgAspect > canvasAspect) { |
|
|
|
drawHeight = slideshowCanvas.height; |
|
drawWidth = drawHeight * imgAspect; |
|
offsetX = (slideshowCanvas.width - drawWidth) / 2; |
|
offsetY = 0; |
|
} else { |
|
|
|
drawWidth = slideshowCanvas.width; |
|
drawHeight = drawWidth / imgAspect; |
|
offsetX = 0; |
|
offsetY = (slideshowCanvas.height - drawHeight) / 2; |
|
} |
|
|
|
ctx.clearRect(0, 0, slideshowCanvas.width, slideshowCanvas.height); |
|
ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight); |
|
} |
|
|
|
|
|
function applyTransition(type, duration) { |
|
return new Promise((resolve) => { |
|
const steps = 30; |
|
const stepDuration = duration / steps; |
|
|
|
let currentStep = 0; |
|
|
|
function animateTransition() { |
|
if (currentStep >= steps) { |
|
resolve(); |
|
return; |
|
} |
|
|
|
const progress = currentStep / steps; |
|
|
|
switch(type) { |
|
case 'fade': |
|
ctx.globalAlpha = 1 - progress; |
|
break; |
|
case 'slide': |
|
ctx.translate(slideshowCanvas.width * progress, 0); |
|
break; |
|
} |
|
|
|
currentStep++; |
|
setTimeout(animateTransition, stepDuration); |
|
} |
|
|
|
animateTransition(); |
|
}); |
|
} |
|
|
|
|
|
function simulateAIProcessing(messages, duration, transition, style, imageCount) { |
|
return new Promise((resolve) => { |
|
let progress = 0; |
|
const totalSteps = messages.length; |
|
let currentStep = 0; |
|
|
|
const interval = setInterval(() => { |
|
progress += 100 / totalSteps; |
|
currentStep++; |
|
|
|
|
|
progressBar.style.width = `${progress}%`; |
|
progressPercent.textContent = `${Math.min(Math.round(progress), 100)}%`; |
|
|
|
|
|
aiStatus.textContent = messages[currentStep - 1]; |
|
progressText.textContent = `Step ${currentStep} of ${totalSteps}`; |
|
|
|
|
|
if (progress >= 100) { |
|
clearInterval(interval); |
|
resolve(); |
|
} |
|
}, 1500); |
|
}); |
|
} |
|
|
|
|
|
downloadBtn.addEventListener('click', function() { |
|
if (!videoBlob) { |
|
showError("The video is not ready yet. Please wait for processing to complete."); |
|
return; |
|
} |
|
|
|
try { |
|
const a = document.createElement('a'); |
|
const url = URL.createObjectURL(videoBlob); |
|
a.href = url; |
|
a.download = `ai-slideshow-${new Date().toISOString().slice(0, 10)}.${videoFormat.value}`; |
|
document.body.appendChild(a); |
|
a.click(); |
|
|
|
|
|
setTimeout(() => { |
|
document.body.removeChild(a); |
|
URL.revokeObjectURL(url); |
|
}, 100); |
|
|
|
addAIMessage("Your download has started! Would you like me to create another slideshow with different settings?"); |
|
} catch (error) { |
|
console.error('Error downloading video:', error); |
|
showError("Failed to download the video. Please try again."); |
|
} |
|
}); |
|
|
|
|
|
regenerateBtn.addEventListener('click', function() { |
|
if (uploadedImages.length === 0) { |
|
showError("Please upload some images first."); |
|
return; |
|
} |
|
|
|
|
|
window.scrollTo({ top: 0, behavior: 'smooth' }); |
|
|
|
|
|
addAIMessage("I'll regenerate your slideshow with new settings. You can adjust the options before creating it again."); |
|
|
|
|
|
setTimeout(() => { |
|
createVideoBtn.click(); |
|
}, 1000); |
|
}); |
|
|
|
|
|
function addUserMessage(message) { |
|
const messageDiv = document.createElement('div'); |
|
messageDiv.className = 'chat-message user-message'; |
|
messageDiv.textContent = message; |
|
chatContainer.appendChild(messageDiv); |
|
chatContainer.scrollTop = chatContainer.scrollHeight; |
|
} |
|
|
|
function addAIMessage(message) { |
|
const messageDiv = document.createElement('div'); |
|
messageDiv.className = 'chat-message ai-message'; |
|
|
|
|
|
const typingIndicator = document.createElement('div'); |
|
typingIndicator.className = 'flex space-x-1 items-center'; |
|
typingIndicator.innerHTML = ` |
|
<div class="w-2 h-2 bg-gray-400 rounded-full ai-processing"></div> |
|
<div class="w-2 h-2 bg-gray-400 rounded-full ai-processing" style="animation-delay: 0.2s"></div> |
|
<div class="w-2 h-2 bg-gray-400 rounded-full ai-processing" style="animation-delay: 0.4s"></div> |
|
`; |
|
messageDiv.appendChild(typingIndicator); |
|
chatContainer.appendChild(messageDiv); |
|
chatContainer.scrollTop = chatContainer.scrollHeight; |
|
|
|
|
|
setTimeout(() => { |
|
messageDiv.innerHTML = ''; |
|
messageDiv.textContent = message; |
|
chatContainer.scrollTop = chatContainer.scrollHeight; |
|
}, 1000 + Math.random() * 1000); |
|
} |
|
|
|
|
|
function sendChatMessage() { |
|
const message = chatInput.value.trim(); |
|
if (message === '') return; |
|
|
|
addUserMessage(message); |
|
chatInput.value = ''; |
|
|
|
|
|
const responses = [ |
|
"I'll help you with that! Based on your images, I recommend using the 'Cinematic' style for a professional look.", |
|
"For a romantic feel, try setting the slide duration to 5 seconds and selecting the 'Fade' transition.", |
|
"I can enhance your dark photos automatically. Just make sure 'Color Correct' is checked in the options.", |
|
"The best transitions depend on your content. Fade works well for most cases, while slide gives a dynamic feel.", |
|
"You've uploaded great images! I suggest trying the 'Modern' style to make them pop.", |
|
"For a faster-paced slideshow, reduce the slide duration to 2-3 seconds and enable random order.", |
|
"I've analyzed your images and will automatically crop them to consistent ratios for the best presentation.", |
|
"To create a shorter clip, select 'Short Segment' in the video options. I'll use the first 10 images." |
|
]; |
|
|
|
|
|
setTimeout(() => { |
|
const randomResponse = responses[Math.floor(Math.random() * responses.length)]; |
|
addAIMessage(randomResponse); |
|
|
|
|
|
if (Math.random() > 0.7) { |
|
setTimeout(() => { |
|
addAIMessage("Would you like me to apply these suggestions to your current slideshow settings?"); |
|
}, 1500); |
|
} |
|
}, 1000); |
|
} |
|
|
|
|
|
chatInput.addEventListener('keypress', function(e) { |
|
if (e.key === 'Enter') { |
|
sendChatMessage(); |
|
} |
|
}); |
|
|
|
sendChatBtn.addEventListener('click', sendChatMessage); |
|
|
|
|
|
quickPrompts.forEach(button => { |
|
button.addEventListener('click', function() { |
|
chatInput.value = this.textContent; |
|
sendChatMessage(); |
|
}); |
|
}); |
|
|
|
|
|
setTimeout(() => { |
|
addAIMessage("I see you've opened the slideshow creator! I can help you make an amazing video. Upload some images and tell me what kind of slideshow you're looking for."); |
|
}, 1000); |
|
|
|
|
|
videoPreview.addEventListener('error', function() { |
|
showError("There was an issue playing the video. Try regenerating or downloading it instead."); |
|
addAIMessage("Sorry about the playback issue! I recommend downloading the video to view it properly."); |
|
}); |
|
}); |
|
</script> |
|
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=XXXMARK/karua" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
</html> |