| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Video Clip Generator</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> |
| .video-container { |
| position: relative; |
| padding-bottom: 56.25%; |
| height: 0; |
| overflow: hidden; |
| } |
| .video-container video { |
| position: absolute; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| } |
| .clip-card { |
| transition: all 0.3s ease; |
| } |
| .clip-card:hover { |
| transform: translateY(-5px); |
| box-shadow: 0 10px 20px rgba(0,0,0,0.1); |
| } |
| .progress-bar { |
| height: 4px; |
| background-color: #e5e7eb; |
| position: relative; |
| } |
| .progress-fill { |
| position: absolute; |
| top: 0; |
| left: 0; |
| height: 100%; |
| background-color: #3b82f6; |
| transition: width 0.3s ease; |
| } |
| .waveform { |
| height: 60px; |
| background: linear-gradient(90deg, #e5e7eb 50%, transparent 50%); |
| background-size: 10px 100%; |
| position: relative; |
| } |
| .waveform-active { |
| background: linear-gradient(90deg, #3b82f6 50%, transparent 50%); |
| background-size: 10px 100%; |
| } |
| .file-info { |
| display: none; |
| } |
| .ffmpeg-loading { |
| position: fixed; |
| top: 0; |
| left: 0; |
| right: 0; |
| bottom: 0; |
| background-color: rgba(255,255,255,0.8); |
| display: flex; |
| flex-direction: column; |
| justify-content: center; |
| align-items: center; |
| z-index: 1000; |
| } |
| .ffmpeg-progress { |
| width: 80%; |
| max-width: 400px; |
| margin-top: 20px; |
| } |
| </style> |
| </head> |
| <body class="bg-gray-50 min-h-screen"> |
| <div id="ffmpegLoading" class="ffmpeg-loading"> |
| <div class="text-center"> |
| <div class="animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-blue-500 mb-4"></div> |
| <h2 class="text-xl font-semibold text-gray-800 mb-2">Loading Video Processor</h2> |
| <p class="text-gray-600 mb-4">This may take a few moments...</p> |
| <div class="ffmpeg-progress"> |
| <div class="w-full bg-gray-200 rounded-full h-2.5"> |
| <div id="ffmpegProgressBar" class="bg-blue-600 h-2.5 rounded-full" style="width: 0%"></div> |
| </div> |
| <p id="ffmpegProgressText" class="text-sm text-gray-600 mt-2">Initializing...</p> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="container mx-auto px-4 py-8"> |
| <header class="text-center mb-12"> |
| <h1 class="text-4xl font-bold text-gray-800 mb-2">Video Clip Generator</h1> |
| <p class="text-lg text-gray-600">Upload your MP4 video and we'll automatically create interesting clips for you</p> |
| </header> |
|
|
| <div class="max-w-3xl mx-auto bg-white rounded-xl shadow-md overflow-hidden p-6 mb-8"> |
| <div class="text-center mb-6"> |
| <div class="flex justify-center mb-4"> |
| <div class="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center"> |
| <i class="fas fa-video text-blue-500 text-2xl"></i> |
| </div> |
| </div> |
| <h2 class="text-xl font-semibold text-gray-800 mb-2">Upload Your Video</h2> |
| <p class="text-gray-500 mb-4">We'll analyze your video and create clips between 30-60 seconds</p> |
| |
| <div class="border-2 border-dashed border-gray-300 rounded-lg p-8 mb-4" id="dropZone"> |
| <input type="file" id="videoUpload" accept="video/mp4" class="hidden"> |
| <label for="videoUpload" class="cursor-pointer"> |
| <div class="flex flex-col items-center justify-center"> |
| <i class="fas fa-cloud-upload-alt text-4xl text-blue-500 mb-3"></i> |
| <p class="text-gray-600 mb-2">Drag & drop your MP4 file here</p> |
| <p class="text-sm text-gray-400 mb-3">or</p> |
| <button type="button" class="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-6 rounded-full transition duration-300"> |
| Browse Files |
| </button> |
| </div> |
| </label> |
| <div id="fileInfo" class="file-info mt-4 p-3 bg-gray-50 rounded-lg"> |
| <div class="flex items-center"> |
| <i class="fas fa-file-video text-blue-500 mr-3"></i> |
| <div class="text-left"> |
| <p class="font-medium text-gray-800" id="fileName"></p> |
| <p class="text-sm text-gray-500" id="fileSize"></p> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| <div id="uploadProgress" class="hidden"> |
| <div class="flex items-center justify-between mb-2"> |
| <span class="text-sm font-medium text-gray-700">Uploading...</span> |
| <span class="text-sm font-medium text-gray-700" id="progressPercent">0%</span> |
| </div> |
| <div class="progress-bar w-full rounded-full"> |
| <div class="progress-fill rounded-full" id="progressFill" style="width: 0%"></div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <div id="videoProcessing" class="hidden max-w-3xl mx-auto bg-white rounded-xl shadow-md overflow-hidden p-6 mb-8"> |
| <div class="flex items-center justify-center"> |
| <div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500 mr-4"></div> |
| <div> |
| <h3 class="text-lg font-medium text-gray-800">Processing your video...</h3> |
| <p class="text-gray-500">We're analyzing your video to find the best clips</p> |
| </div> |
| </div> |
| </div> |
|
|
| <div id="videoPreviewSection" class="hidden max-w-3xl mx-auto bg-white rounded-xl shadow-md overflow-hidden p-6 mb-8"> |
| <h2 class="text-xl font-semibold text-gray-800 mb-4">Your Video</h2> |
| <div class="video-container mb-4 rounded-lg overflow-hidden"> |
| <video id="videoPlayer" controls class="w-full"></video> |
| </div> |
| <div class="flex justify-between items-center"> |
| <span class="text-sm text-gray-500" id="videoDuration">Duration: 0:00</span> |
| <button id="analyzeBtn" class="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded transition duration-300"> |
| <i class="fas fa-magic mr-2"></i> Generate Clips |
| </button> |
| </div> |
| </div> |
|
|
| <div id="clipsSection" class="hidden max-w-4xl mx-auto"> |
| <h2 class="text-2xl font-semibold text-gray-800 mb-6">Generated Clips</h2> |
| |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6" id="clipsContainer"> |
| |
| </div> |
| </div> |
| </div> |
|
|
| <script src="https://unpkg.com/@ffmpeg/ffmpeg@0.11.6/dist/ffmpeg.min.js"></script> |
| <script src="https://unpkg.com/@ffmpeg/core@0.11.0/dist/ffmpeg-core.js"></script> |
| <script> |
| document.addEventListener('DOMContentLoaded', function() { |
| const videoUpload = document.getElementById('videoUpload'); |
| const dropZone = document.getElementById('dropZone'); |
| const uploadProgress = document.getElementById('uploadProgress'); |
| const progressFill = document.getElementById('progressFill'); |
| const progressPercent = document.getElementById('progressPercent'); |
| const videoProcessing = document.getElementById('videoProcessing'); |
| const videoPreviewSection = document.getElementById('videoPreviewSection'); |
| const videoPlayer = document.getElementById('videoPlayer'); |
| const videoDuration = document.getElementById('videoDuration'); |
| const analyzeBtn = document.getElementById('analyzeBtn'); |
| const clipsSection = document.getElementById('clipsSection'); |
| const clipsContainer = document.getElementById('clipsContainer'); |
| const fileInfo = document.getElementById('fileInfo'); |
| const fileName = document.getElementById('fileName'); |
| const fileSize = document.getElementById('fileSize'); |
| const ffmpegLoading = document.getElementById('ffmpegLoading'); |
| const ffmpegProgressBar = document.getElementById('ffmpegProgressBar'); |
| const ffmpegProgressText = document.getElementById('ffmpegProgressText'); |
| |
| let videoFile = null; |
| let videoUrl = null; |
| let ffmpeg = null; |
| |
| |
| async function initFFmpeg() { |
| const { createFFmpeg, fetchFile } = FFmpeg; |
| ffmpeg = createFFmpeg({ |
| log: true, |
| corePath: 'https://unpkg.com/@ffmpeg/core@0.11.0/dist/ffmpeg-core.js' |
| }); |
| |
| |
| ffmpeg.setProgress(({ ratio }) => { |
| const percent = Math.round(ratio * 100); |
| ffmpegProgressBar.style.width = `${percent}%`; |
| ffmpegProgressText.textContent = `Loading: ${percent}%`; |
| }); |
| |
| try { |
| await ffmpeg.load(); |
| ffmpegProgressBar.style.width = '100%'; |
| ffmpegProgressText.textContent = 'Ready!'; |
| |
| |
| setTimeout(() => { |
| ffmpegLoading.style.display = 'none'; |
| }, 500); |
| } catch (error) { |
| console.error('FFmpeg loading error:', error); |
| ffmpegProgressText.textContent = 'Error loading FFmpeg. Please refresh the page.'; |
| } |
| } |
| |
| initFFmpeg(); |
| |
| |
| ['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('border-blue-500'); |
| dropZone.classList.remove('border-gray-300'); |
| } |
| |
| function unhighlight() { |
| dropZone.classList.remove('border-blue-500'); |
| dropZone.classList.add('border-gray-300'); |
| } |
| |
| dropZone.addEventListener('drop', handleDrop, false); |
| |
| function handleDrop(e) { |
| const dt = e.dataTransfer; |
| const files = dt.files; |
| if (files.length > 0 && files[0].type === 'video/mp4') { |
| handleFiles(files); |
| } else { |
| alert('Please upload an MP4 video file.'); |
| } |
| } |
| |
| |
| videoUpload.addEventListener('change', function(e) { |
| if (this.files.length > 0) { |
| if (this.files[0].type === 'video/mp4') { |
| handleFiles(this.files); |
| } else { |
| alert('Please upload an MP4 video file.'); |
| this.value = ''; |
| } |
| } |
| }); |
| |
| function handleFiles(files) { |
| videoFile = files[0]; |
| |
| |
| fileName.textContent = videoFile.name; |
| fileSize.textContent = formatFileSize(videoFile.size); |
| fileInfo.style.display = 'block'; |
| |
| |
| uploadProgress.classList.remove('hidden'); |
| |
| |
| let progress = 0; |
| const interval = setInterval(() => { |
| progress += Math.random() * 10; |
| if (progress >= 100) { |
| progress = 100; |
| clearInterval(interval); |
| |
| |
| setTimeout(() => { |
| uploadProgress.classList.add('hidden'); |
| processVideo(); |
| }, 500); |
| } |
| |
| progressFill.style.width = `${progress}%`; |
| progressPercent.textContent = `${Math.round(progress)}%`; |
| }, 200); |
| } |
| |
| function formatFileSize(bytes) { |
| if (bytes === 0) return '0 Bytes'; |
| const k = 1024; |
| const sizes = ['Bytes', 'KB', 'MB', 'GB']; |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; |
| } |
| |
| function processVideo() { |
| videoProcessing.classList.remove('hidden'); |
| |
| |
| if (videoUrl) { |
| URL.revokeObjectURL(videoUrl); |
| } |
| videoUrl = URL.createObjectURL(videoFile); |
| |
| |
| videoPlayer.src = videoUrl; |
| |
| |
| videoPlayer.onloadedmetadata = function() { |
| videoProcessing.classList.add('hidden'); |
| videoPreviewSection.classList.remove('hidden'); |
| |
| |
| const duration = videoPlayer.duration; |
| const minutes = Math.floor(duration / 60); |
| const seconds = Math.floor(duration % 60); |
| videoDuration.textContent = `Duration: ${minutes}:${seconds < 10 ? '0' + seconds : seconds}`; |
| }; |
| |
| videoPlayer.onerror = function() { |
| videoProcessing.classList.add('hidden'); |
| alert('Error loading video. Please try another file.'); |
| }; |
| } |
| |
| |
| analyzeBtn.addEventListener('click', function() { |
| videoPreviewSection.classList.add('hidden'); |
| videoProcessing.classList.remove('hidden'); |
| |
| |
| setTimeout(() => { |
| generateClips(); |
| }, 1000); |
| }); |
| |
| async function generateClips() { |
| |
| clipsContainer.innerHTML = ''; |
| |
| const duration = videoPlayer.duration; |
| const clipCount = Math.max(1, Math.floor(duration / 45)); |
| |
| |
| for (let i = 0; i < clipCount; i++) { |
| const clipDuration = 30 + Math.random() * 30; |
| const startTime = Math.min(i * (duration / clipCount), duration - clipDuration); |
| const endTime = Math.min(startTime + clipDuration, duration); |
| |
| createClipCard(i + 1, startTime, endTime); |
| } |
| |
| videoProcessing.classList.add('hidden'); |
| clipsSection.classList.remove('hidden'); |
| } |
| |
| function createClipCard(index, startTime, endTime) { |
| const card = document.createElement('div'); |
| card.className = 'clip-card bg-white rounded-lg shadow-md overflow-hidden'; |
| |
| |
| const formatTime = (time) => { |
| const minutes = Math.floor(time / 60); |
| const seconds = Math.floor(time % 60); |
| return `${minutes}:${seconds < 10 ? '0' + seconds : seconds}`; |
| }; |
| |
| |
| const waveform = document.createElement('div'); |
| waveform.className = 'waveform mb-2'; |
| |
| |
| const activeWaveform = document.createElement('div'); |
| activeWaveform.className = 'waveform-active absolute top-0 left-0'; |
| activeWaveform.style.width = `${(endTime - startTime) / videoPlayer.duration * 100}%`; |
| activeWaveform.style.left = `${startTime / videoPlayer.duration * 100}%`; |
| waveform.appendChild(activeWaveform); |
| |
| card.innerHTML = ` |
| <div class="p-4"> |
| <div class="flex justify-between items-center mb-3"> |
| <h3 class="text-lg font-medium text-gray-800">Clip #${index}</h3> |
| <span class="text-sm bg-blue-100 text-blue-600 py-1 px-2 rounded-full"> |
| ${formatTime(endTime - startTime)} |
| </span> |
| </div> |
| <div class="mb-3"> |
| ${waveform.outerHTML} |
| <div class="flex justify-between text-xs text-gray-500"> |
| <span>${formatTime(startTime)}</span> |
| <span>${formatTime(endTime)}</span> |
| </div> |
| </div> |
| <div class="flex justify-between items-center"> |
| <button class="preview-clip text-blue-500 hover:text-blue-600 font-medium flex items-center" |
| data-start="${startTime}" data-end="${endTime}"> |
| <i class="fas fa-play mr-2"></i> Preview |
| </button> |
| <button class="download-clip bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded transition duration-300 flex items-center" |
| data-start="${startTime}" data-end="${endTime}"> |
| <i class="fas fa-download mr-2"></i> Download |
| </button> |
| </div> |
| </div> |
| `; |
| |
| clipsContainer.appendChild(card); |
| } |
| |
| |
| document.addEventListener('click', function(e) { |
| if (e.target.classList.contains('preview-clip') || e.target.parentElement.classList.contains('preview-clip')) { |
| const button = e.target.classList.contains('preview-clip') ? e.target : e.target.parentElement; |
| const startTime = parseFloat(button.getAttribute('data-start')); |
| const endTime = parseFloat(button.getAttribute('data-end')); |
| |
| videoPlayer.currentTime = startTime; |
| videoPlayer.play(); |
| |
| |
| const checkTime = setInterval(() => { |
| if (videoPlayer.currentTime >= endTime) { |
| videoPlayer.pause(); |
| clearInterval(checkTime); |
| } |
| }, 100); |
| } |
| |
| if (e.target.classList.contains('download-clip') || e.target.parentElement.classList.contains('download-clip')) { |
| const button = e.target.classList.contains('download-clip') ? e.target : e.target.parentElement; |
| const startTime = parseFloat(button.getAttribute('data-start')); |
| const endTime = parseFloat(button.getAttribute('data-end')); |
| |
| downloadClip(startTime, endTime, button); |
| } |
| }); |
| |
| async function downloadClip(startTime, endTime, button) { |
| if (!ffmpeg || !ffmpeg.isLoaded()) { |
| alert('Video processor is still loading. Please wait a moment and try again.'); |
| return; |
| } |
| |
| const originalHTML = button.innerHTML; |
| button.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> Processing...'; |
| button.disabled = true; |
| |
| try { |
| |
| const statusElement = document.createElement('div'); |
| statusElement.className = 'text-xs text-gray-500 mt-1'; |
| statusElement.textContent = 'Preparing clip...'; |
| button.parentNode.appendChild(statusElement); |
| |
| |
| statusElement.textContent = 'Reading video file...'; |
| await ffmpeg.writeFile('input.mp4', await fetchFile(videoFile)); |
| |
| |
| statusElement.textContent = 'Processing video...'; |
| await ffmpeg.exec([ |
| '-i', 'input.mp4', |
| '-ss', startTime.toString(), |
| '-to', endTime.toString(), |
| '-c', 'copy', |
| 'output.mp4' |
| ]); |
| |
| |
| statusElement.textContent = 'Finalizing clip...'; |
| const data = await ffmpeg.readFile('output.mp4'); |
| |
| |
| const blob = new Blob([data.buffer], { type: 'video/mp4' }); |
| const url = URL.createObjectURL(blob); |
| const a = document.createElement('a'); |
| a.href = url; |
| a.download = `clip_${Math.floor(startTime)}_${Math.floor(endTime)}.mp4`; |
| document.body.appendChild(a); |
| a.click(); |
| document.body.removeChild(a); |
| |
| |
| URL.revokeObjectURL(url); |
| statusElement.remove(); |
| |
| |
| button.innerHTML = '<i class="fas fa-check mr-2"></i> Downloaded!'; |
| button.classList.remove('bg-blue-500', 'hover:bg-blue-600'); |
| button.classList.add('bg-green-500', 'hover:bg-green-600'); |
| |
| setTimeout(() => { |
| button.innerHTML = originalHTML; |
| button.classList.remove('bg-green-500', 'hover:bg-green-600'); |
| button.classList.add('bg-blue-500', 'hover:bg-blue-600'); |
| button.disabled = false; |
| }, 2000); |
| } catch (error) { |
| console.error('Error processing video:', error); |
| button.innerHTML = '<i class="fas fa-times mr-2"></i> Error'; |
| button.classList.remove('bg-blue-500', 'hover:bg-blue-600'); |
| button.classList.add('bg-red-500', 'hover:bg-red-600'); |
| |
| setTimeout(() => { |
| button.innerHTML = originalHTML; |
| button.classList.remove('bg-red-500', 'hover:bg-red-600'); |
| button.classList.add('bg-blue-500', 'hover:bg-blue-600'); |
| button.disabled = false; |
| }, 2000); |
| } |
| } |
| |
| |
| async function fetchFile(file) { |
| return new Uint8Array(await readFileAsArrayBuffer(file)); |
| } |
| |
| function readFileAsArrayBuffer(file) { |
| return new Promise((resolve, reject) => { |
| const reader = new FileReader(); |
| reader.onload = () => resolve(reader.result); |
| reader.onerror = reject; |
| reader.readAsArrayBuffer(file); |
| }); |
| } |
| }); |
| </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=beppe1234/space" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body> |
| </html> |