space / index.html
beppe1234's picture
Add 2 files
d3babb3 verified
<!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%; /* 16:9 aspect ratio */
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">
<!-- Clips will be added here dynamically -->
</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;
// Initialize FFmpeg
async function initFFmpeg() {
const { createFFmpeg, fetchFile } = FFmpeg;
ffmpeg = createFFmpeg({
log: true,
corePath: 'https://unpkg.com/@ffmpeg/core@0.11.0/dist/ffmpeg-core.js'
});
// Show loading progress
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!';
// Hide loading screen after a short delay
setTimeout(() => {
ffmpegLoading.style.display = 'none';
}, 500);
} catch (error) {
console.error('FFmpeg loading error:', error);
ffmpegProgressText.textContent = 'Error loading FFmpeg. Please refresh the page.';
}
}
initFFmpeg();
// Handle drag and drop
['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.');
}
}
// Handle file selection
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 = ''; // Clear the input
}
}
});
function handleFiles(files) {
videoFile = files[0];
// Show file info
fileName.textContent = videoFile.name;
fileSize.textContent = formatFileSize(videoFile.size);
fileInfo.style.display = 'block';
// Show upload progress
uploadProgress.classList.remove('hidden');
// Simulate upload progress (in a real app, you'd use XMLHttpRequest or Fetch API)
let progress = 0;
const interval = setInterval(() => {
progress += Math.random() * 10;
if (progress >= 100) {
progress = 100;
clearInterval(interval);
// Upload complete
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');
// Create object URL for the video
if (videoUrl) {
URL.revokeObjectURL(videoUrl);
}
videoUrl = URL.createObjectURL(videoFile);
// Set video source
videoPlayer.src = videoUrl;
// Wait for video metadata to load
videoPlayer.onloadedmetadata = function() {
videoProcessing.classList.add('hidden');
videoPreviewSection.classList.remove('hidden');
// Display video duration
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.');
};
}
// Analyze button click
analyzeBtn.addEventListener('click', function() {
videoPreviewSection.classList.add('hidden');
videoProcessing.classList.remove('hidden');
// Process the video to generate clips
setTimeout(() => {
generateClips();
}, 1000);
});
async function generateClips() {
// Clear previous clips
clipsContainer.innerHTML = '';
const duration = videoPlayer.duration;
const clipCount = Math.max(1, Math.floor(duration / 45)); // Aim for ~45 second clips
// Generate random clips between 30-60 seconds
for (let i = 0; i < clipCount; i++) {
const clipDuration = 30 + Math.random() * 30; // 30-60 seconds
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';
// Format times for display
const formatTime = (time) => {
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return `${minutes}:${seconds < 10 ? '0' + seconds : seconds}`;
};
// Create waveform visualization
const waveform = document.createElement('div');
waveform.className = 'waveform mb-2';
// Create active portion of waveform
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);
}
// Handle preview and download buttons
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();
// Pause at end time
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 {
// Show processing status
const statusElement = document.createElement('div');
statusElement.className = 'text-xs text-gray-500 mt-1';
statusElement.textContent = 'Preparing clip...';
button.parentNode.appendChild(statusElement);
// Write the video file to FFmpeg's virtual file system
statusElement.textContent = 'Reading video file...';
await ffmpeg.writeFile('input.mp4', await fetchFile(videoFile));
// Run FFmpeg command to extract the clip
statusElement.textContent = 'Processing video...';
await ffmpeg.exec([
'-i', 'input.mp4',
'-ss', startTime.toString(),
'-to', endTime.toString(),
'-c', 'copy',
'output.mp4'
]);
// Read the result
statusElement.textContent = 'Finalizing clip...';
const data = await ffmpeg.readFile('output.mp4');
// Create a download link
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);
// Clean up
URL.revokeObjectURL(url);
statusElement.remove();
// Show download confirmation
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);
}
}
// Helper function to fetch file as Uint8Array
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>