Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>AI Video Highlight 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"> | |
| <!-- YouTube Iframe API --> | |
| <script src="https://www.youtube.com/iframe_api"></script> | |
| <style> | |
| .waveform { | |
| height: 100px; | |
| background: linear-gradient(90deg, #4f46e5 0%, #10b981 100%); | |
| border-radius: 8px; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .waveform-progress { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| height: 100%; | |
| width: 0; | |
| background-color: rgba(255, 255, 255, 0.2); | |
| pointer-events: none; | |
| } | |
| .waveform-marker { | |
| position: absolute; | |
| top: 0; | |
| height: 100%; | |
| width: 2px; | |
| background-color: white; | |
| z-index: 10; | |
| } | |
| .highlight-clip { | |
| position: absolute; | |
| height: 100%; | |
| background-color: rgba(255, 255, 0, 0.3); | |
| border-left: 2px solid yellow; | |
| border-right: 2px solid yellow; | |
| } | |
| .subtitle-display { | |
| background-color: rgba(0, 0, 0, 0.7); | |
| border-radius: 8px; | |
| padding: 12px 24px; | |
| max-width: 80%; | |
| margin: 0 auto; | |
| font-size: 1.2rem; | |
| text-align: center; | |
| transition: all 0.3s ease; | |
| } | |
| .loading-spinner { | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .file-upload-label:hover { | |
| background-color: #4f46e5; | |
| } | |
| .youtube-input:hover { | |
| background-color: #10b981; | |
| } | |
| #youtubePlayer { | |
| width: 100%; | |
| aspect-ratio: 16/9; | |
| background: #000; | |
| } | |
| .player-container { | |
| position: relative; | |
| } | |
| .player-overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| pointer-events: none; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| flex-direction: column; | |
| background: rgba(0,0,0,0.5); | |
| z-index: 10; | |
| } | |
| .thumbnail-overlay { | |
| background-size: cover; | |
| background-position: center; | |
| } | |
| .export-progress { | |
| transition: width 0.3s ease; | |
| } | |
| .clip-preview { | |
| position: relative; | |
| } | |
| .clip-preview:hover .clip-overlay { | |
| opacity: 1; | |
| } | |
| .clip-overlay { | |
| opacity: 0; | |
| transition: opacity 0.3s ease; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-900 text-gray-100 min-h-screen"> | |
| <div class="container mx-auto px-4 py-8"> | |
| <header class="text-center mb-12"> | |
| <h1 class="text-4xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-emerald-400 mb-2"> | |
| AI Video Highlight Generator | |
| </h1> | |
| <p class="text-lg text-gray-400 max-w-2xl mx-auto"> | |
| Upload a video or paste a YouTube URL to automatically extract 1-minute highlight clips with AI analysis. | |
| </p> | |
| </header> | |
| <div class="max-w-4xl mx-auto bg-gray-800 rounded-xl shadow-2xl overflow-hidden"> | |
| <!-- Input Section --> | |
| <div class="p-6 border-b border-gray-700"> | |
| <div class="flex flex-col md:flex-row gap-4"> | |
| <!-- File Upload --> | |
| <div class="flex-1"> | |
| <input type="file" id="fileInput" accept="video/*" class="hidden"> | |
| <label for="fileInput" class="file-upload-label flex flex-col items-center justify-center p-8 border-2 border-dashed border-purple-500 rounded-lg cursor-pointer hover:bg-purple-900/20 transition-colors"> | |
| <i class="fas fa-cloud-upload-alt text-4xl text-purple-400 mb-3"></i> | |
| <h3 class="text-xl font-semibold mb-1">Upload Video</h3> | |
| <p class="text-gray-400 text-sm">MP4, MOV, AVI up to 100MB</p> | |
| </label> | |
| </div> | |
| <!-- Or Divider --> | |
| <div class="flex items-center justify-center"> | |
| <div class="h-px w-16 bg-gray-600 md:h-16 md:w-px"></div> | |
| </div> | |
| <!-- YouTube Input --> | |
| <div class="flex-1"> | |
| <div class="youtube-input flex flex-col h-full p-4 border-2 border-dashed border-emerald-500 rounded-lg hover:bg-emerald-900/20 transition-colors"> | |
| <div class="flex items-center mb-3"> | |
| <i class="fab fa-youtube text-4xl text-red-500 mr-3"></i> | |
| <h3 class="text-xl font-semibold">YouTube URL</h3> | |
| </div> | |
| <div class="flex gap-2"> | |
| <input type="text" id="youtubeUrl" placeholder="https://youtube.com/watch?v=..." class="flex-1 bg-gray-700 rounded px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-emerald-500"> | |
| <button id="loadYoutubeBtn" class="bg-emerald-600 hover:bg-emerald-500 px-4 py-2 rounded font-medium transition-colors"> | |
| Load | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="uploadProgress" class="hidden mt-4"> | |
| <div class="flex items-center gap-3 text-purple-400"> | |
| <i class="fas fa-spinner loading-spinner"></i> | |
| <span id="progressText">Processing your video...</span> | |
| </div> | |
| <div id="progressBar" class="w-full bg-gray-700 rounded-full h-2.5 mt-2"> | |
| <div id="progressBarFill" class="bg-emerald-500 h-2.5 rounded-full" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Video Player Section --> | |
| <div id="videoSection" class="hidden p-6 border-b border-gray-700"> | |
| <div class="player-container"> | |
| <div id="youtubePlayer"></div> | |
| <video id="videoPlayer" controls class="w-full hidden rounded-lg bg-black aspect-video"></video> | |
| <div id="playerOverlay" class="player-overlay hidden"> | |
| <div class="text-xl font-semibold mb-2">Video Preview</div> | |
| <button id="playVideoBtn" class="bg-purple-600 hover:bg-purple-500 w-16 h-16 rounded-full flex items-center justify-center transition-colors pointer-events-auto"> | |
| <i class="fas fa-play text-2xl"></i> | |
| </button> | |
| </div> | |
| <div id="subtitleContainer" class="subtitle-display hidden absolute bottom-8 left-0 right-0"></div> | |
| </div> | |
| <div class="flex justify-between items-center mt-4"> | |
| <div class="flex items-center gap-4"> | |
| <button id="playBtn" class="bg-purple-600 hover:bg-purple-500 w-12 h-12 rounded-full flex items-center justify-center transition-colors"> | |
| <i class="fas fa-play"></i> | |
| </button> | |
| <button id="pauseBtn" class="bg-gray-700 hover:bg-gray-600 w-12 h-12 rounded-full flex items-center justify-center transition-colors"> | |
| <i class="fas fa-pause"></i> | |
| </button> | |
| <div class="text-sm text-gray-400"> | |
| <span id="currentTime">00:00</span> / <span id="duration">00:00</span> | |
| </div> | |
| </div> | |
| <button id="processBtn" class="bg-gradient-to-r from-purple-600 to-emerald-600 hover:from-purple-500 hover:to-emerald-500 px-6 py-3 rounded-full font-medium transition-all transform hover:scale-105"> | |
| <i class="fas fa-magic mr-2"></i> Generate 1-Min Highlights | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Waveform & Highlights Section --> | |
| <div id="waveformContainer" class="hidden p-6 border-b border-gray-700"> | |
| <h3 class="text-lg font-semibold mb-4">Video Analysis</h3> | |
| <div class="waveform relative cursor-pointer mb-4"> | |
| <div class="waveform-progress"></div> | |
| <div class="waveform-marker"></div> | |
| <!-- Highlight clips will be added here by JavaScript --> | |
| </div> | |
| <div id="highlightClips" class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <!-- Highlight clips will be added here by JavaScript --> | |
| </div> | |
| </div> | |
| <!-- Results Section --> | |
| <div id="resultsSection" class="hidden p-6"> | |
| <div class="flex justify-between items-center mb-6"> | |
| <h3 class="text-xl font-semibold">Your 1-Minute Highlights</h3> | |
| <button id="downloadAllBtn" class="bg-gradient-to-r from-purple-600 to-emerald-600 hover:from-purple-500 hover:to-emerald-500 px-6 py-3 rounded-full font-medium transition-all flex items-center gap-2"> | |
| <i class="fas fa-download"></i> Download All (ZIP) | |
| </button> | |
| </div> | |
| <div id="exportStatus" class="hidden mb-4 bg-gray-700 rounded-lg p-4"> | |
| <div class="flex justify-between items-center mb-2"> | |
| <span class="font-medium">Preparing clips for download...</span> | |
| <span id="exportPercent" class="font-bold">0%</span> | |
| </div> | |
| <div class="w-full bg-gray-600 rounded-full h-2.5"> | |
| <div id="exportProgress" class="bg-emerald-500 h-2.5 rounded-full export-progress" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| <div id="highlightResults" class="grid grid-cols-1 gap-6"> | |
| <!-- Highlight results will be added here by JavaScript --> | |
| </div> | |
| </div> | |
| <!-- Empty State --> | |
| <div id="emptyState" class="p-12 text-center"> | |
| <i class="fas fa-video text-6xl text-gray-600 mb-4"></i> | |
| <h3 class="text-xl font-semibold mb-2">No Video Loaded</h3> | |
| <p class="text-gray-500 max-w-md mx-auto"> | |
| Upload a video file or paste a YouTube URL above to get started. | |
| </p> | |
| </div> | |
| </div> | |
| <div class="mt-12 text-center text-gray-500 text-sm"> | |
| <p>Powered by AI video analysis technology. Processing may take a few moments.</p> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // DOM Elements | |
| const fileInput = document.getElementById('fileInput'); | |
| const youtubeUrl = document.getElementById('youtubeUrl'); | |
| const loadYoutubeBtn = document.getElementById('loadYoutubeBtn'); | |
| const youtubePlayerElement = document.getElementById('youtubePlayer'); | |
| const videoPlayer = document.getElementById('videoPlayer'); | |
| const playBtn = document.getElementById('playBtn'); | |
| const pauseBtn = document.getElementById('pauseBtn'); | |
| const playVideoBtn = document.getElementById('playVideoBtn'); | |
| const processBtn = document.getElementById('processBtn'); | |
| const downloadAllBtn = document.getElementById('downloadAllBtn'); | |
| const currentTimeEl = document.getElementById('currentTime'); | |
| const durationEl = document.getElementById('duration'); | |
| const waveformContainer = document.getElementById('waveformContainer'); | |
| const waveform = document.querySelector('.waveform'); | |
| let waveformProgress = document.querySelector('.waveform-progress'); | |
| let waveformMarker = document.querySelector('.waveform-marker'); | |
| const highlightClips = document.getElementById('highlightClips'); | |
| const highlightResults = document.getElementById('highlightResults'); | |
| const subtitleContainer = document.getElementById('subtitleContainer'); | |
| const videoSection = document.getElementById('videoSection'); | |
| const emptyState = document.getElementById('emptyState'); | |
| const uploadProgress = document.getElementById('uploadProgress'); | |
| const progressBarFill = document.getElementById('progressBarFill'); | |
| const progressText = document.getElementById('progressText'); | |
| const resultsSection = document.getElementById('resultsSection'); | |
| const playerOverlay = document.getElementById('playerOverlay'); | |
| const exportStatus = document.getElementById('exportStatus'); | |
| const exportProgress = document.getElementById('exportProgress'); | |
| const exportPercent = document.getElementById('exportPercent'); | |
| // State variables | |
| let videoFile = null; | |
| let videoBlobUrl = null; | |
| let videoType = null; // 'file' or 'youtube' | |
| let videoDuration = 0; | |
| let isProcessing = false; | |
| let highlights = []; | |
| let subtitles = []; | |
| let waveformData = []; | |
| let youtubePlayer = null; | |
| let isYouTubePlaying = false; | |
| let youtubeCurrentTimer = null; | |
| let youtubeCurrentTime = 0; | |
| let youtubeThumbnail = ''; | |
| let youtubeTitle = ''; | |
| let youtubeVideoId = ''; | |
| // Event Listeners | |
| fileInput.addEventListener('change', handleFileSelect); | |
| loadYoutubeBtn.addEventListener('click', useYouTubeVideo); | |
| playBtn.addEventListener('click', playVideo); | |
| pauseBtn.addEventListener('click', pauseVideo); | |
| playVideoBtn.addEventListener('click', playVideo); | |
| processBtn.addEventListener('click', processVideo); | |
| downloadAllBtn.addEventListener('click', handleDownloadAll); | |
| videoPlayer.addEventListener('timeupdate', updateVideoTime); | |
| videoPlayer.addEventListener('ended', () => { | |
| playBtn.innerHTML = '<i class="fas fa-redo"></i>'; | |
| }); | |
| waveform.addEventListener('click', handleWaveformClick); | |
| // YouTube API callback | |
| window.onYouTubeIframeAPIReady = function() { | |
| // Player will be created when YouTube URL is loaded | |
| }; | |
| // Functions | |
| function handleFileSelect(e) { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| if (!file.type.startsWith('video/')) { | |
| alert('Please select a video file'); | |
| return; | |
| } | |
| // Reset any existing YouTube player | |
| if (youtubePlayer) { | |
| youtubePlayer.destroy(); | |
| youtubePlayer = null; | |
| youtubePlayerElement.innerHTML = ''; | |
| youtubePlayerElement.classList.add('hidden'); | |
| } | |
| videoFile = file; | |
| videoType = 'file'; | |
| showUploadProgress('Processing video file...'); | |
| // Display video | |
| videoBlobUrl = URL.createObjectURL(file); | |
| setupFileVideoPlayer(); | |
| } | |
| function useYouTubeVideo() { | |
| const url = youtubeUrl.value.trim(); | |
| if (!url) { | |
| alert('Please enter a YouTube URL'); | |
| return; | |
| } | |
| // Extract video ID from URL | |
| youtubeVideoId = extractYouTubeId(url); | |
| if (!youtubeVideoId) { | |
| alert('Please enter a valid YouTube URL'); | |
| return; | |
| } | |
| // Clean up any existing video | |
| if (videoBlobUrl) { | |
| URL.revokeObjectURL(videoBlobUrl); | |
| videoBlobUrl = null; | |
| } | |
| videoPlayer.src = ''; | |
| videoPlayer.classList.add('hidden'); | |
| videoType = 'youtube'; | |
| showUploadProgress('Loading YouTube video...'); | |
| // Load YouTube player | |
| loadYouTubeVideo(youtubeVideoId); | |
| } | |
| function extractYouTubeId(url) { | |
| const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; | |
| const match = url.match(regExp); | |
| return (match && match[2].length === 11) ? match[2] : null; | |
| } | |
| function loadYouTubeVideo(videoId) { | |
| // First get video info | |
| fetch(`https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoId}&format=json`) | |
| .then(response => { | |
| if (!response.ok) throw new Error('Invalid YouTube URL'); | |
| return response.json(); | |
| }) | |
| .then(data => { | |
| youtubeTitle = data.title; | |
| youtubeThumbnail = `https://img.youtube.com/vi/${videoId}/mqdefault.jpg`; | |
| // Set up player overlay | |
| playerOverlay.style.background = `url('${youtubeThumbnail}') center/cover no-repeat`; | |
| playerOverlay.classList.remove('hidden'); | |
| // Update progress bar | |
| updateProgress(50, 'Loading YouTube player...'); | |
| // Create YouTube player | |
| if (!window.YT) { | |
| throw new Error('YouTube API not loaded'); | |
| } | |
| youtubePlayer = new YT.Player('youtubePlayer', { | |
| height: '100%', | |
| width: '100%', | |
| videoId: videoId, | |
| playerVars: { | |
| 'autoplay': 0, | |
| 'controls': 0, | |
| 'disablekb': 1, | |
| 'modestbranding': 1, | |
| 'rel': 0 | |
| }, | |
| events: { | |
| 'onReady': onYouTubePlayerReady, | |
| 'onStateChange': onYouTubePlayerStateChange, | |
| 'onError': onYouTubePlayerError | |
| } | |
| }); | |
| youtubePlayerElement.classList.remove('hidden'); | |
| youtubePlayerElement.classList.remove('hidden'); | |
| }) | |
| .catch(error => { | |
| console.error('Error loading YouTube video:', error); | |
| alert('Error loading YouTube video. Please check the URL and try again.'); | |
| uploadProgress.classList.add('hidden'); | |
| }); | |
| } | |
| function onYouTubePlayerReady(event) { | |
| updateProgress(80, 'Preparing video...'); | |
| setTimeout(() => { | |
| // Get duration when metadata is available | |
| setTimeout(() => { | |
| videoDuration = event.target.getDuration(); | |
| if (videoDuration && !isNaN(videoDuration)) { | |
| showVideoUI(); | |
| uploadProgress.classList.add('hidden'); | |
| } else { | |
| // If duration isn't available right away, keep checking | |
| const interval = setInterval(() => { | |
| const duration = event.target.getDuration(); | |
| if (duration && !isNaN(duration)) { | |
| clearInterval(interval); | |
| videoDuration = duration; | |
| showVideoUI(); | |
| uploadProgress.classList.add('hidden'); | |
| } | |
| }, 500); | |
| } | |
| }, 500); | |
| }, 1000); | |
| } | |
| function onYouTubePlayerStateChange(event) { | |
| switch(event.data) { | |
| case YT.PlayerState.PLAYING: | |
| isYouTubePlaying = true; | |
| startYouTubeTimeTracker(); | |
| playerOverlay.classList.add('hidden'); | |
| break; | |
| case YT.PlayerState.PAUSED: | |
| isYouTubePlaying = false; | |
| stopYouTubeTimeTracker(); | |
| playerOverlay.classList.remove('hidden'); | |
| playBtn.innerHTML = '<i class="fas fa-play"></i>'; | |
| break; | |
| case YT.PlayerState.ENDED: | |
| isYouTubePlaying = false; | |
| stopYouTubeTimeTracker(); | |
| playerOverlay.classList.remove('hidden'); | |
| playBtn.innerHTML = '<i class="fas fa-redo"></i>'; | |
| break; | |
| } | |
| } | |
| function onYouTubePlayerError(event) { | |
| console.error('YouTube Player Error:', event.data); | |
| alert('Error loading YouTube video. Please try again with a different video.'); | |
| uploadProgress.classList.add('hidden'); | |
| } | |
| function startYouTubeTimeTracker() { | |
| stopYouTubeTimeTracker(); | |
| youtubeCurrentTimer = setInterval(() => { | |
| if (youtubePlayer && youtubePlayer.getCurrentTime) { | |
| youtubeCurrentTime = youtubePlayer.getCurrentTime(); | |
| updateCurrentTimeDisplay(youtubeCurrentTime); | |
| updateWaveformProgress(youtubeCurrentTime); | |
| updateSubtitles(youtubeCurrentTime); | |
| } | |
| }, 200); | |
| } | |
| function stopYouTubeTimeTracker() { | |
| if (youtubeCurrentTimer) { | |
| clearInterval(youtubeCurrentTimer); | |
| youtubeCurrentTimer = null; | |
| } | |
| } | |
| function updateCurrentTimeDisplay(currentTime) { | |
| const minutes = Math.floor(currentTime / 60); | |
| const seconds = Math.floor(currentTime % 60); | |
| currentTimeEl.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; | |
| } | |
| function updateWaveformProgress(currentTime) { | |
| if (!videoDuration) return; | |
| const progressPercent = (currentTime / videoDuration) * 100; | |
| waveformProgress.style.width = `${progressPercent}%`; | |
| waveformMarker.style.left = `${progressPercent}%`; | |
| } | |
| function updateSubtitles(currentTime) { | |
| if (subtitles.length > 0) { | |
| const currentSubtitle = subtitles.find(sub => | |
| currentTime >= sub.start && currentTime <= sub.end | |
| ); | |
| if (currentSubtitle) { | |
| subtitleContainer.textContent = currentSubtitle.text; | |
| subtitleContainer.classList.remove('hidden'); | |
| } else { | |
| subtitleContainer.classList.add('hidden'); | |
| } | |
| } | |
| } | |
| function setupFileVideoPlayer() { | |
| videoPlayer.src = videoBlobUrl; | |
| videoPlayer.classList.remove('hidden'); | |
| playerOverlay.classList.add('hidden'); | |
| processBtn.disabled = false; | |
| playBtn.disabled = false; | |
| pauseBtn.disabled = false; | |
| waveformContainer.classList.remove('hidden'); | |
| videoSection.classList.remove('hidden'); | |
| emptyState.classList.add('hidden'); | |
| // Wait for metadata to load | |
| videoPlayer.onloadedmetadata = () => { | |
| videoDuration = videoPlayer.duration; | |
| updateDurationDisplay(); | |
| generateWaveformData(); | |
| renderWaveform(); | |
| updateVideoTime(); | |
| // Hide upload progress | |
| uploadProgress.classList.add('hidden'); | |
| }; | |
| // Handle errors | |
| videoPlayer.onerror = () => { | |
| alert('Error loading video file'); | |
| uploadProgress.classList.add('hidden'); | |
| }; | |
| } | |
| function showVideoUI() { | |
| updateDurationDisplay(); | |
| generateWaveformData(); | |
| renderWaveform(); | |
| updateCurrentTimeDisplay(0); | |
| processBtn.disabled = false; | |
| playBtn.disabled = false; | |
| pauseBtn.disabled = false; | |
| waveformContainer.classList.remove('hidden'); | |
| videoSection.classList.remove('hidden'); | |
| emptyState.classList.add('hidden'); | |
| } | |
| function showUploadProgress(text) { | |
| progressText.textContent = text; | |
| progressBarFill.style.width = '10%'; | |
| uploadProgress.classList.remove('hidden'); | |
| processBtn.disabled = true; | |
| } | |
| function updateProgress(percent, text) { | |
| progressBarFill.style.width = `${percent}%`; | |
| if (text) progressText.textContent = text; | |
| } | |
| function playVideo() { | |
| if (videoType === 'youtube' && youtubePlayer) { | |
| youtubePlayer.playVideo(); | |
| isYouTubePlaying = true; | |
| playerOverlay.classList.add('hidden'); | |
| playBtn.innerHTML = '<i class="fas fa-pause"></i>'; | |
| } else if (videoType === 'file') { | |
| videoPlayer.play(); | |
| playBtn.innerHTML = '<i class="fas fa-pause"></i>'; | |
| } | |
| } | |
| function pauseVideo() { | |
| if (videoType === 'youtube' && youtubePlayer) { | |
| youtubePlayer.pauseVideo(); | |
| isYouTubePlaying = false; | |
| playerOverlay.classList.remove('hidden'); | |
| playBtn.innerHTML = '<i class="fas fa-play"></i>'; | |
| } else if (videoType === 'file') { | |
| videoPlayer.pause(); | |
| playBtn.innerHTML = '<i class="fas fa-play"></i>'; | |
| } | |
| } | |
| function updateDurationDisplay() { | |
| if (isNaN(videoDuration) || !isFinite(videoDuration)) { | |
| durationEl.textContent = '00:00'; | |
| return; | |
| } | |
| const minutes = Math.floor(videoDuration / 60); | |
| const seconds = Math.floor(videoDuration % 60); | |
| durationEl.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; | |
| } | |
| function updateVideoTime() { | |
| const currentTime = videoPlayer.currentTime; | |
| const minutes = Math.floor(currentTime / 60); | |
| const seconds = Math.floor(currentTime % 60); | |
| currentTimeEl.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; | |
| // Update waveform progress | |
| const progressPercent = (currentTime / videoDuration) * 100; | |
| waveformProgress.style.width = `${progressPercent}%`; | |
| waveformMarker.style.left = `${progressPercent}%`; | |
| // Update subtitle display if we have subtitles | |
| updateSubtitles(currentTime); | |
| } | |
| function handleWaveformClick(e) { | |
| const rect = waveform.getBoundingClientRect(); | |
| const clickPosition = e.clientX - rect.left; | |
| const percent = clickPosition / rect.width; | |
| const seekTime = percent * videoDuration; | |
| if (videoType === 'youtube' && youtubePlayer) { | |
| youtubePlayer.seekTo(seekTime, true); | |
| youtubeCurrentTime = seekTime; | |
| updateCurrentTimeDisplay(seekTime); | |
| updateWaveformProgress(seekTime); | |
| updateSubtitles(seekTime); | |
| } else if (videoType === 'file') { | |
| videoPlayer.currentTime = seekTime; | |
| } | |
| } | |
| function generateWaveformData() { | |
| // In a real app, you would analyze the audio to generate waveform data | |
| // For this demo, we'll generate random data | |
| waveformData = []; | |
| const segments = 200; | |
| for (let i = 0; i < segments; i++) { | |
| waveformData.push(Math.random() * 0.8 + 0.2); // Values between 0.2 and 1.0 | |
| } | |
| } | |
| function renderWaveform() { | |
| // Clear any existing highlights | |
| waveform.innerHTML = ` | |
| <div class="waveform-progress"></div> | |
| <div class="waveform-marker"></div> | |
| `; | |
| // Get references again after clearing | |
| waveformProgress = document.querySelector('.waveform-progress'); | |
| waveformMarker = document.querySelector('.waveform-marker'); | |
| // Create waveform visualization | |
| const segmentWidth = waveform.offsetWidth / waveformData.length; | |
| waveformData.forEach((value, index) => { | |
| const bar = document.createElement('div'); | |
| bar.className = 'absolute bottom-0 bg-purple-400'; | |
| bar.style.left = `${index * segmentWidth}px`; | |
| bar.style.width = `${segmentWidth}px`; | |
| bar.style.height = `${value * 100}%`; | |
| waveform.appendChild(bar); | |
| }); | |
| } | |
| function processVideo() { | |
| if (isProcessing) return; | |
| if (videoDuration < 60) { | |
| alert('Video must be at least 1 minute long to generate highlights'); | |
| return; | |
| } | |
| isProcessing = true; | |
| // Show processing state | |
| processBtn.innerHTML = '<i class="fas fa-spinner loading-spinner mr-2"></i> Processing...'; | |
| processBtn.disabled = true; | |
| // Simulate AI processing delay | |
| setTimeout(() => { | |
| // Generate mock highlights and subtitles | |
| generateMockHighlights(); | |
| generateMockSubtitles(); | |
| // Display highlights on waveform | |
| displayHighlightsOnWaveform(); | |
| // Display highlight clips | |
| displayHighlightClips(); | |
| // Show results section | |
| resultsSection.classList.remove('hidden'); | |
| // Reset button | |
| processBtn.innerHTML = '<i class="fas fa-magic mr-2"></i> Generate 1-Min Highlights'; | |
| processBtn.disabled = false; | |
| isProcessing = false; | |
| }, 3000); | |
| } | |
| function generateMockHighlights() { | |
| // In a real app, this would come from your AI analysis | |
| highlights = []; | |
| // Determine how many 1-minute highlights we can have | |
| const maxHighlights = Math.floor(videoDuration / 60); | |
| const highlightCount = Math.min(maxHighlights, 3); // Max 3 highlights for demo | |
| // Generate highlights at approximately 25%, 50%, 75% of video | |
| for (let i = 0; i < highlightCount; i++) { | |
| // Get a position in the video (25%, 50%, 75%) | |
| const positionPercent = 0.25 + (0.25 * i); | |
| let start = positionPercent * videoDuration; | |
| // Make sure highlight doesn't go past end of video | |
| start = Math.min(start, videoDuration - 60); | |
| highlights.push({ | |
| start: start, | |
| end: start + 60, // Exactly 1 minute | |
| confidence: Math.random() * 0.5 + 0.5, // 0.5-1.0 | |
| title: `Best Moment ${i+1}`, | |
| description: `Watch this exciting 1-minute highlight reel from the video. Automatically generated by AI analysis.`, | |
| id: `highlight_${Date.now()}_${i}`, | |
| previewImage: videoType === 'youtube' ? | |
| `https://img.youtube.com/vi/${youtubeVideoId}/mqdefault.jpg` : | |
| (videoBlobUrl ? videoBlobUrl : '') | |
| }); | |
| } | |
| // Sort by start time | |
| highlights.sort((a, b) => a.start - b.start); | |
| } | |
| function generateMockSubtitles() { | |
| // In a real app, this would come from speech-to-text | |
| subtitles = []; | |
| // Generate subtitles for each highlight | |
| highlights.forEach((highlight, i) => { | |
| const textOptions = [ | |
| "This is the most exciting part of the video!", | |
| "The best 1-minute segment from this content.", | |
| "AI selected this as the most engaging moment.", | |
| "Highlight reel of the most important content.", | |
| "This 60-second clip contains the key takeaways." | |
| ]; | |
| subtitles.push({ | |
| start: highlight.start, | |
| end: highlight.end, | |
| text: textOptions[i % textOptions.length] | |
| }); | |
| }); | |
| } | |
| function displayHighlightsOnWaveform() { | |
| highlights.forEach(highlight => { | |
| const clip = document.createElement('div'); | |
| clip.className = 'highlight-clip'; | |
| clip.style.left = `${(highlight.start / videoDuration) * 100}%`; | |
| clip.style.width = `${((highlight.end - highlight.start) / videoDuration) * 100}%`; | |
| clip.title = `1-min Highlight: ${formatTime(highlight.start)} - ${formatTime(highlight.end)}`; | |
| clip.dataset.clipId = highlight.id; | |
| waveform.appendChild(clip); | |
| // Make highlight clip clickable | |
| clip.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| seekToHighlight(highlight.start); | |
| }); | |
| }); | |
| } | |
| function seekToHighlight(time) { | |
| if (videoType === 'youtube' && youtubePlayer) { | |
| youtubePlayer.seekTo(time, true); | |
| if (!isYouTubePlaying) { | |
| youtubePlayer.playVideo(); | |
| } | |
| } else if (videoType === 'file') { | |
| videoPlayer.currentTime = time; | |
| if (videoPlayer.paused) { | |
| videoPlayer.play(); | |
| } | |
| } | |
| } | |
| function displayHighlightClips() { | |
| highlightClips.innerHTML = ''; | |
| highlightResults.innerHTML = ''; | |
| const titleBase = videoType === 'youtube' ? youtubeTitle : 'Your Video'; | |
| highlights.forEach((highlight, index) => { | |
| // Create clip card for waveform section | |
| const clipCard = document.createElement('div'); | |
| clipCard.className = 'bg-gray-700 rounded-lg p-4 flex items-start gap-4 hover:bg-gray-600 transition-colors cursor-pointer'; | |
| clipCard.addEventListener('click', () => seekToHighlight(highlight.start)); | |
| clipCard.innerHTML = ` | |
| <div class="bg-purple-600/20 w-12 h-12 rounded-full flex items-center justify-center flex-shrink-0"> | |
| <span class="text-xl font-bold">${index+1}</span> | |
| </div> | |
| <div> | |
| <h4 class="font-semibold mb-1">${highlight.title}</h4> | |
| <p class="text-sm text-gray-400 mb-2">${formatTime(highlight.start)} - ${formatTime(highlight.end)} (1 min)</p> | |
| <p class="text-sm">${highlight.description}</p> | |
| </div> | |
| `; | |
| highlightClips.appendChild(clipCard); | |
| // Create result card for results section | |
| const resultCard = document.createElement('div'); | |
| resultCard.className = 'bg-gray-800 rounded-xl overflow-hidden border border-gray-700 clip-preview'; | |
| resultCard.setAttribute('data-clip-id', highlight.id); | |
| resultCard.innerHTML = ` | |
| <div class="relative"> | |
| <div class="w-full bg-black aspect-video flex items-center justify-center relative"> | |
| ${videoType === 'youtube' ? | |
| `<img src="${highlight.previewImage}" alt="${titleBase}" class="w-full h-full object-cover"> | |
| <div class="clip-overlay absolute inset-0 flex items-center justify-center bg-black/50"> | |
| <div class="bg-white/20 rounded-full w-16 h-16 flex items-center justify-center backdrop-blur-sm"> | |
| <i class="fas fa-play text-white text-2xl"></i> | |
| </div> | |
| </div>` : | |
| `<video class="w-full h-full" muted> | |
| <source src="${videoBlobUrl}" type="video/mp4"> | |
| </video> | |
| <div class="clip-overlay absolute inset-0 flex items-center justify-center bg-black/50"> | |
| <div class="bg-white/20 rounded-full w-16 h-16 flex items-center justify-center backdrop-blur-sm"> | |
| <i class="fas fa-play text-white text-2xl"></i> | |
| </div> | |
| </div>`} | |
| </div> | |
| <div class="absolute bottom-4 left-0 right-0 px-4"> | |
| <div class="bg-black/70 text-white rounded px-3 py-2 text-center max-w-md mx-auto"> | |
| ${subtitles[index]?.text || '1-minute highlight'} | |
| </div> | |
| </div> | |
| </div> | |
| <div class="p-4"> | |
| <div class="flex justify-between items-start mb-2"> | |
| <div> | |
| <h4 class="font-semibold text-lg">${titleBase}</h4> | |
| <p class="text-sm text-gray-400">${highlight.title}</p> | |
| </div> | |
| <button class="download-clip-btn bg-gray-700 hover:bg-gray-600 px-4 py-2 rounded font-medium flex items-center gap-2" data-clip-id="${highlight.id}"> | |
| <i class="fas fa-download"></i> Download | |
| </button> | |
| </div> | |
| <p class="text-sm text-gray-400 mb-2">${formatTime(highlight.start)} - ${formatTime(highlight.end)} (1 min)</p> | |
| <p class="text-sm">${highlight.description}</p> | |
| </div> | |
| `; | |
| highlightResults.appendChild(resultCard); | |
| // Add click handler to preview the clip | |
| const videoPreview = resultCard.querySelector('video, img'); | |
| videoPreview.parentElement.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| seekToHighlight(highlight.start); | |
| }); | |
| }); | |
| // Add event listeners to download buttons | |
| document.querySelectorAll('.download-clip-btn').forEach(btn => { | |
| btn.addEventListener('click', function(e) { | |
| e.stopPropagation(); | |
| const clipId = this.getAttribute('data-clip-id'); | |
| downloadHighlight(clipId); | |
| }); | |
| }); | |
| } | |
| function formatTime(seconds) { | |
| const mins = Math.floor(seconds / 60); | |
| const secs = Math.floor(seconds % 60); | |
| return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; | |
| } | |
| function downloadHighlight(clipId) { | |
| const highlight = highlights.find(h => h.id === clipId); | |
| if (!highlight) return; | |
| // Show exporting status | |
| exportStatus.classList.remove('hidden'); | |
| exportProgress.style.width = '0%'; | |
| exportPercent.textContent = '0%'; | |
| // Simulate export progress | |
| let progress = 0; | |
| const interval = setInterval(() => { | |
| progress += Math.random() * 10; | |
| if (progress >= 100) { | |
| progress = 100; | |
| clearInterval(interval); | |
| // After a small delay, show completion | |
| setTimeout(() => { | |
| completeExport(highlight); | |
| }, 500); | |
| } | |
| exportProgress.style.width = `${progress}%`; | |
| exportPercent.textContent = `${Math.floor(progress)}%`; | |
| }, 100); | |
| } | |
| function completeExport(highlight) { | |
| exportProgress.style.width = '100%'; | |
| exportPercent.textContent = '100%'; | |
| setTimeout(() => { | |
| // Create a dummy download link (in a real app, this would be your exported clip) | |
| const dummyDownloadUrl = videoType === 'youtube' ? | |
| `https://www.youtube.com/watch?v=${youtubeVideoId}&t=${Math.floor(highlight.start)}` : | |
| videoBlobUrl; | |
| const a = document.createElement('a'); | |
| a.href = dummyDownloadUrl; | |
| a.download = `highlight_${formatTime(highlight.start)}_${formatTime(highlight.end)}.mp4`; | |
| a.style.display = 'none'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| exportStatus.classList.add('hidden'); | |
| if (videoType === 'youtube') { | |
| alert(`In a real application, this would download the 1-minute YouTube clip from ${formatTime(highlight.start)} to ${formatTime(highlight.end)}. For now, it links to the YouTube video at the start time.`); | |
| } else { | |
| alert(`In a real application, this would download just the 1-minute clip from ${formatTime(highlight.start)} to ${formatTime(highlight.end)}. For demo purposes, it downloads the full video.`); | |
| } | |
| }, 500); | |
| } | |
| function handleDownloadAll() { | |
| if (highlights.length === 0) return; | |
| // Show exporting status | |
| exportStatus.classList.remove('hidden'); | |
| exportProgress.style.width = '0%'; | |
| exportPercent.textContent = '0%'; | |
| // Simulate export progress for all clips | |
| let progress = 0; | |
| const interval = setInterval(() => { | |
| progress += Math.random() * 5; | |
| if (progress >= 100) { | |
| progress = 100; | |
| clearInterval(interval); | |
| setTimeout(() => { | |
| completeAllExport(); | |
| }, 500); | |
| } | |
| exportProgress.style.width = `${progress}%`; | |
| exportPercent.textContent = `${Math.floor(progress)}%`; | |
| }, 100); | |
| } | |
| function completeAllExport() { | |
| exportProgress.style.width = '100%'; | |
| exportPercent.textContent = '100%'; | |
| setTimeout(() => { | |
| // Create a dummy ZIP download (in a real app, this would package all clips) | |
| let downloadUrl; | |
| let downloadText; | |
| if (videoType === 'youtube') { | |
| downloadUrl = `https://www.youtube.com/watch?v=${youtubeVideoId}`; | |
| downloadText = `In a real application, this would download all 1-minute YouTube highlights as a ZIP file. For now, it links to the full YouTube video.`; | |
| } else { | |
| downloadUrl = videoBlobUrl; | |
| downloadText = `In a real application, this would download all 1-minute highlights as a ZIP file. For demo purposes, it downloads the full video.`; | |
| } | |
| const a = document.createElement('a'); | |
| a.href = downloadUrl; | |
| a.download = `highlights_${videoType === 'youtube' ? youtubeTitle : 'your_video'}.zip`; | |
| a.style.display = 'none'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| exportStatus.classList.add('hidden'); | |
| alert(downloadText); | |
| }, 500); | |
| } | |
| }); | |
| </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/videoaieditor" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body> | |
| </html> |