Spaces:
Running
Running
Add 1 files
Browse files- index.html +226 -59
index.html
CHANGED
|
@@ -89,6 +89,19 @@
|
|
| 89 |
background-size: cover;
|
| 90 |
background-position: center;
|
| 91 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
</style>
|
| 93 |
</head>
|
| 94 |
<body class="bg-gray-900 text-gray-100 min-h-screen">
|
|
@@ -98,7 +111,7 @@
|
|
| 98 |
AI Video Highlight Generator
|
| 99 |
</h1>
|
| 100 |
<p class="text-lg text-gray-400 max-w-2xl mx-auto">
|
| 101 |
-
Upload a video or paste a YouTube URL to automatically extract
|
| 102 |
</p>
|
| 103 |
</header>
|
| 104 |
|
|
@@ -177,7 +190,7 @@
|
|
| 177 |
</div>
|
| 178 |
|
| 179 |
<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">
|
| 180 |
-
<i class="fas fa-magic mr-2"></i> Generate Highlights
|
| 181 |
</button>
|
| 182 |
</div>
|
| 183 |
</div>
|
|
@@ -199,12 +212,22 @@
|
|
| 199 |
<!-- Results Section -->
|
| 200 |
<div id="resultsSection" class="hidden p-6">
|
| 201 |
<div class="flex justify-between items-center mb-6">
|
| 202 |
-
<h3 class="text-xl font-semibold">Your Highlights</h3>
|
| 203 |
-
<button id="
|
| 204 |
-
<i class="fas fa-download"></i> Download All
|
| 205 |
</button>
|
| 206 |
</div>
|
| 207 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
<div id="highlightResults" class="grid grid-cols-1 gap-6">
|
| 209 |
<!-- Highlight results will be added here by JavaScript -->
|
| 210 |
</div>
|
|
@@ -237,7 +260,7 @@
|
|
| 237 |
const pauseBtn = document.getElementById('pauseBtn');
|
| 238 |
const playVideoBtn = document.getElementById('playVideoBtn');
|
| 239 |
const processBtn = document.getElementById('processBtn');
|
| 240 |
-
const
|
| 241 |
const currentTimeEl = document.getElementById('currentTime');
|
| 242 |
const durationEl = document.getElementById('duration');
|
| 243 |
const waveformContainer = document.getElementById('waveformContainer');
|
|
@@ -254,6 +277,9 @@
|
|
| 254 |
const progressText = document.getElementById('progressText');
|
| 255 |
const resultsSection = document.getElementById('resultsSection');
|
| 256 |
const playerOverlay = document.getElementById('playerOverlay');
|
|
|
|
|
|
|
|
|
|
| 257 |
|
| 258 |
// State variables
|
| 259 |
let videoFile = null;
|
|
@@ -270,6 +296,7 @@
|
|
| 270 |
let youtubeCurrentTime = 0;
|
| 271 |
let youtubeThumbnail = '';
|
| 272 |
let youtubeTitle = '';
|
|
|
|
| 273 |
|
| 274 |
// Event Listeners
|
| 275 |
fileInput.addEventListener('change', handleFileSelect);
|
|
@@ -278,7 +305,7 @@
|
|
| 278 |
pauseBtn.addEventListener('click', pauseVideo);
|
| 279 |
playVideoBtn.addEventListener('click', playVideo);
|
| 280 |
processBtn.addEventListener('click', processVideo);
|
| 281 |
-
|
| 282 |
videoPlayer.addEventListener('timeupdate', updateVideoTime);
|
| 283 |
videoPlayer.addEventListener('ended', () => {
|
| 284 |
playBtn.innerHTML = '<i class="fas fa-redo"></i>';
|
|
@@ -325,8 +352,8 @@
|
|
| 325 |
}
|
| 326 |
|
| 327 |
// Extract video ID from URL
|
| 328 |
-
|
| 329 |
-
if (!
|
| 330 |
alert('Please enter a valid YouTube URL');
|
| 331 |
return;
|
| 332 |
}
|
|
@@ -343,7 +370,7 @@
|
|
| 343 |
showUploadProgress('Loading YouTube video...');
|
| 344 |
|
| 345 |
// Load YouTube player
|
| 346 |
-
loadYouTubeVideo(
|
| 347 |
}
|
| 348 |
|
| 349 |
function extractYouTubeId(url) {
|
|
@@ -482,6 +509,7 @@
|
|
| 482 |
}
|
| 483 |
|
| 484 |
function updateWaveformProgress(currentTime) {
|
|
|
|
| 485 |
const progressPercent = (currentTime / videoDuration) * 100;
|
| 486 |
waveformProgress.style.width = `${progressPercent}%`;
|
| 487 |
waveformMarker.style.left = `${progressPercent}%`;
|
|
@@ -661,6 +689,11 @@
|
|
| 661 |
|
| 662 |
function processVideo() {
|
| 663 |
if (isProcessing) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 664 |
isProcessing = true;
|
| 665 |
|
| 666 |
// Show processing state
|
|
@@ -683,7 +716,7 @@
|
|
| 683 |
resultsSection.classList.remove('hidden');
|
| 684 |
|
| 685 |
// Reset button
|
| 686 |
-
processBtn.innerHTML = '<i class="fas fa-magic mr-2"></i> Generate Highlights';
|
| 687 |
processBtn.disabled = false;
|
| 688 |
isProcessing = false;
|
| 689 |
}, 3000);
|
|
@@ -693,20 +726,29 @@
|
|
| 693 |
// In a real app, this would come from your AI analysis
|
| 694 |
highlights = [];
|
| 695 |
|
| 696 |
-
//
|
| 697 |
-
const
|
|
|
|
| 698 |
|
|
|
|
| 699 |
for (let i = 0; i < highlightCount; i++) {
|
| 700 |
-
|
| 701 |
-
const
|
| 702 |
-
|
|
|
|
|
|
|
|
|
|
| 703 |
|
| 704 |
highlights.push({
|
| 705 |
-
start,
|
| 706 |
-
end: start +
|
| 707 |
confidence: Math.random() * 0.5 + 0.5, // 0.5-1.0
|
| 708 |
-
title: `
|
| 709 |
-
description: `
|
|
|
|
|
|
|
|
|
|
|
|
|
| 710 |
});
|
| 711 |
}
|
| 712 |
|
|
@@ -721,11 +763,11 @@
|
|
| 721 |
// Generate subtitles for each highlight
|
| 722 |
highlights.forEach((highlight, i) => {
|
| 723 |
const textOptions = [
|
| 724 |
-
"This is
|
| 725 |
-
"The
|
| 726 |
-
"
|
| 727 |
-
"
|
| 728 |
-
"This clip contains the
|
| 729 |
];
|
| 730 |
|
| 731 |
subtitles.push({
|
|
@@ -742,11 +784,32 @@
|
|
| 742 |
clip.className = 'highlight-clip';
|
| 743 |
clip.style.left = `${(highlight.start / videoDuration) * 100}%`;
|
| 744 |
clip.style.width = `${((highlight.end - highlight.start) / videoDuration) * 100}%`;
|
| 745 |
-
clip.title = `Highlight: ${formatTime(highlight.start)} - ${formatTime(highlight.end)}`;
|
|
|
|
| 746 |
waveform.appendChild(clip);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 747 |
});
|
| 748 |
}
|
| 749 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 750 |
function displayHighlightClips() {
|
| 751 |
highlightClips.innerHTML = '';
|
| 752 |
highlightResults.innerHTML = '';
|
|
@@ -756,14 +819,15 @@
|
|
| 756 |
highlights.forEach((highlight, index) => {
|
| 757 |
// Create clip card for waveform section
|
| 758 |
const clipCard = document.createElement('div');
|
| 759 |
-
clipCard.className = 'bg-gray-700 rounded-lg p-4 flex items-start gap-4';
|
|
|
|
| 760 |
clipCard.innerHTML = `
|
| 761 |
<div class="bg-purple-600/20 w-12 h-12 rounded-full flex items-center justify-center flex-shrink-0">
|
| 762 |
<span class="text-xl font-bold">${index+1}</span>
|
| 763 |
</div>
|
| 764 |
<div>
|
| 765 |
<h4 class="font-semibold mb-1">${highlight.title}</h4>
|
| 766 |
-
<p class="text-sm text-gray-400 mb-2">${formatTime(highlight.start)} - ${formatTime(highlight.end)}</p>
|
| 767 |
<p class="text-sm">${highlight.description}</p>
|
| 768 |
</div>
|
| 769 |
`;
|
|
@@ -771,46 +835,63 @@
|
|
| 771 |
|
| 772 |
// Create result card for results section
|
| 773 |
const resultCard = document.createElement('div');
|
| 774 |
-
resultCard.className = 'bg-gray-800 rounded-xl overflow-hidden border border-gray-700';
|
|
|
|
| 775 |
resultCard.innerHTML = `
|
| 776 |
<div class="relative">
|
| 777 |
<div class="w-full bg-black aspect-video flex items-center justify-center relative">
|
| 778 |
${videoType === 'youtube' ?
|
| 779 |
-
`<img src="${
|
| 780 |
-
<div class="absolute inset-0 flex items-center justify-center">
|
| 781 |
-
<div class="bg-
|
| 782 |
<i class="fas fa-play text-white text-2xl"></i>
|
| 783 |
</div>
|
| 784 |
</div>` :
|
| 785 |
`<video class="w-full h-full" muted>
|
| 786 |
<source src="${videoBlobUrl}" type="video/mp4">
|
| 787 |
-
</video>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 788 |
</div>
|
| 789 |
<div class="absolute bottom-4 left-0 right-0 px-4">
|
| 790 |
<div class="bg-black/70 text-white rounded px-3 py-2 text-center max-w-md mx-auto">
|
| 791 |
-
${subtitles[index]?.text || '
|
| 792 |
</div>
|
| 793 |
</div>
|
| 794 |
</div>
|
| 795 |
<div class="p-4">
|
| 796 |
<div class="flex justify-between items-start mb-2">
|
| 797 |
-
<
|
| 798 |
-
|
| 799 |
-
<
|
|
|
|
|
|
|
|
|
|
| 800 |
</button>
|
| 801 |
</div>
|
| 802 |
-
<p class="text-sm text-gray-400 mb-2">${formatTime(highlight.start)} - ${formatTime(highlight.end)}</p>
|
| 803 |
<p class="text-sm">${highlight.description}</p>
|
| 804 |
</div>
|
| 805 |
`;
|
| 806 |
highlightResults.appendChild(resultCard);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 807 |
});
|
| 808 |
|
| 809 |
// Add event listeners to download buttons
|
| 810 |
document.querySelectorAll('.download-clip-btn').forEach(btn => {
|
| 811 |
-
btn.addEventListener('click', function() {
|
| 812 |
-
|
| 813 |
-
|
|
|
|
| 814 |
});
|
| 815 |
});
|
| 816 |
}
|
|
@@ -821,28 +902,114 @@
|
|
| 821 |
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
| 822 |
}
|
| 823 |
|
| 824 |
-
function downloadHighlight(
|
| 825 |
-
|
|
|
|
| 826 |
|
| 827 |
-
|
|
|
|
|
|
|
|
|
|
| 828 |
|
| 829 |
-
//
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 836 |
}
|
| 837 |
|
| 838 |
-
function
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 846 |
}
|
| 847 |
});
|
| 848 |
</script>
|
|
|
|
| 89 |
background-size: cover;
|
| 90 |
background-position: center;
|
| 91 |
}
|
| 92 |
+
.export-progress {
|
| 93 |
+
transition: width 0.3s ease;
|
| 94 |
+
}
|
| 95 |
+
.clip-preview {
|
| 96 |
+
position: relative;
|
| 97 |
+
}
|
| 98 |
+
.clip-preview:hover .clip-overlay {
|
| 99 |
+
opacity: 1;
|
| 100 |
+
}
|
| 101 |
+
.clip-overlay {
|
| 102 |
+
opacity: 0;
|
| 103 |
+
transition: opacity 0.3s ease;
|
| 104 |
+
}
|
| 105 |
</style>
|
| 106 |
</head>
|
| 107 |
<body class="bg-gray-900 text-gray-100 min-h-screen">
|
|
|
|
| 111 |
AI Video Highlight Generator
|
| 112 |
</h1>
|
| 113 |
<p class="text-lg text-gray-400 max-w-2xl mx-auto">
|
| 114 |
+
Upload a video or paste a YouTube URL to automatically extract 1-minute highlight clips with AI analysis.
|
| 115 |
</p>
|
| 116 |
</header>
|
| 117 |
|
|
|
|
| 190 |
</div>
|
| 191 |
|
| 192 |
<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">
|
| 193 |
+
<i class="fas fa-magic mr-2"></i> Generate 1-Min Highlights
|
| 194 |
</button>
|
| 195 |
</div>
|
| 196 |
</div>
|
|
|
|
| 212 |
<!-- Results Section -->
|
| 213 |
<div id="resultsSection" class="hidden p-6">
|
| 214 |
<div class="flex justify-between items-center mb-6">
|
| 215 |
+
<h3 class="text-xl font-semibold">Your 1-Minute Highlights</h3>
|
| 216 |
+
<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">
|
| 217 |
+
<i class="fas fa-download"></i> Download All (ZIP)
|
| 218 |
</button>
|
| 219 |
</div>
|
| 220 |
|
| 221 |
+
<div id="exportStatus" class="hidden mb-4 bg-gray-700 rounded-lg p-4">
|
| 222 |
+
<div class="flex justify-between items-center mb-2">
|
| 223 |
+
<span class="font-medium">Preparing clips for download...</span>
|
| 224 |
+
<span id="exportPercent" class="font-bold">0%</span>
|
| 225 |
+
</div>
|
| 226 |
+
<div class="w-full bg-gray-600 rounded-full h-2.5">
|
| 227 |
+
<div id="exportProgress" class="bg-emerald-500 h-2.5 rounded-full export-progress" style="width: 0%"></div>
|
| 228 |
+
</div>
|
| 229 |
+
</div>
|
| 230 |
+
|
| 231 |
<div id="highlightResults" class="grid grid-cols-1 gap-6">
|
| 232 |
<!-- Highlight results will be added here by JavaScript -->
|
| 233 |
</div>
|
|
|
|
| 260 |
const pauseBtn = document.getElementById('pauseBtn');
|
| 261 |
const playVideoBtn = document.getElementById('playVideoBtn');
|
| 262 |
const processBtn = document.getElementById('processBtn');
|
| 263 |
+
const downloadAllBtn = document.getElementById('downloadAllBtn');
|
| 264 |
const currentTimeEl = document.getElementById('currentTime');
|
| 265 |
const durationEl = document.getElementById('duration');
|
| 266 |
const waveformContainer = document.getElementById('waveformContainer');
|
|
|
|
| 277 |
const progressText = document.getElementById('progressText');
|
| 278 |
const resultsSection = document.getElementById('resultsSection');
|
| 279 |
const playerOverlay = document.getElementById('playerOverlay');
|
| 280 |
+
const exportStatus = document.getElementById('exportStatus');
|
| 281 |
+
const exportProgress = document.getElementById('exportProgress');
|
| 282 |
+
const exportPercent = document.getElementById('exportPercent');
|
| 283 |
|
| 284 |
// State variables
|
| 285 |
let videoFile = null;
|
|
|
|
| 296 |
let youtubeCurrentTime = 0;
|
| 297 |
let youtubeThumbnail = '';
|
| 298 |
let youtubeTitle = '';
|
| 299 |
+
let youtubeVideoId = '';
|
| 300 |
|
| 301 |
// Event Listeners
|
| 302 |
fileInput.addEventListener('change', handleFileSelect);
|
|
|
|
| 305 |
pauseBtn.addEventListener('click', pauseVideo);
|
| 306 |
playVideoBtn.addEventListener('click', playVideo);
|
| 307 |
processBtn.addEventListener('click', processVideo);
|
| 308 |
+
downloadAllBtn.addEventListener('click', handleDownloadAll);
|
| 309 |
videoPlayer.addEventListener('timeupdate', updateVideoTime);
|
| 310 |
videoPlayer.addEventListener('ended', () => {
|
| 311 |
playBtn.innerHTML = '<i class="fas fa-redo"></i>';
|
|
|
|
| 352 |
}
|
| 353 |
|
| 354 |
// Extract video ID from URL
|
| 355 |
+
youtubeVideoId = extractYouTubeId(url);
|
| 356 |
+
if (!youtubeVideoId) {
|
| 357 |
alert('Please enter a valid YouTube URL');
|
| 358 |
return;
|
| 359 |
}
|
|
|
|
| 370 |
showUploadProgress('Loading YouTube video...');
|
| 371 |
|
| 372 |
// Load YouTube player
|
| 373 |
+
loadYouTubeVideo(youtubeVideoId);
|
| 374 |
}
|
| 375 |
|
| 376 |
function extractYouTubeId(url) {
|
|
|
|
| 509 |
}
|
| 510 |
|
| 511 |
function updateWaveformProgress(currentTime) {
|
| 512 |
+
if (!videoDuration) return;
|
| 513 |
const progressPercent = (currentTime / videoDuration) * 100;
|
| 514 |
waveformProgress.style.width = `${progressPercent}%`;
|
| 515 |
waveformMarker.style.left = `${progressPercent}%`;
|
|
|
|
| 689 |
|
| 690 |
function processVideo() {
|
| 691 |
if (isProcessing) return;
|
| 692 |
+
if (videoDuration < 60) {
|
| 693 |
+
alert('Video must be at least 1 minute long to generate highlights');
|
| 694 |
+
return;
|
| 695 |
+
}
|
| 696 |
+
|
| 697 |
isProcessing = true;
|
| 698 |
|
| 699 |
// Show processing state
|
|
|
|
| 716 |
resultsSection.classList.remove('hidden');
|
| 717 |
|
| 718 |
// Reset button
|
| 719 |
+
processBtn.innerHTML = '<i class="fas fa-magic mr-2"></i> Generate 1-Min Highlights';
|
| 720 |
processBtn.disabled = false;
|
| 721 |
isProcessing = false;
|
| 722 |
}, 3000);
|
|
|
|
| 726 |
// In a real app, this would come from your AI analysis
|
| 727 |
highlights = [];
|
| 728 |
|
| 729 |
+
// Determine how many 1-minute highlights we can have
|
| 730 |
+
const maxHighlights = Math.floor(videoDuration / 60);
|
| 731 |
+
const highlightCount = Math.min(maxHighlights, 3); // Max 3 highlights for demo
|
| 732 |
|
| 733 |
+
// Generate highlights at approximately 25%, 50%, 75% of video
|
| 734 |
for (let i = 0; i < highlightCount; i++) {
|
| 735 |
+
// Get a position in the video (25%, 50%, 75%)
|
| 736 |
+
const positionPercent = 0.25 + (0.25 * i);
|
| 737 |
+
let start = positionPercent * videoDuration;
|
| 738 |
+
|
| 739 |
+
// Make sure highlight doesn't go past end of video
|
| 740 |
+
start = Math.min(start, videoDuration - 60);
|
| 741 |
|
| 742 |
highlights.push({
|
| 743 |
+
start: start,
|
| 744 |
+
end: start + 60, // Exactly 1 minute
|
| 745 |
confidence: Math.random() * 0.5 + 0.5, // 0.5-1.0
|
| 746 |
+
title: `Best Moment ${i+1}`,
|
| 747 |
+
description: `Watch this exciting 1-minute highlight reel from the video. Automatically generated by AI analysis.`,
|
| 748 |
+
id: `highlight_${Date.now()}_${i}`,
|
| 749 |
+
previewImage: videoType === 'youtube' ?
|
| 750 |
+
`https://img.youtube.com/vi/${youtubeVideoId}/mqdefault.jpg` :
|
| 751 |
+
(videoBlobUrl ? videoBlobUrl : '')
|
| 752 |
});
|
| 753 |
}
|
| 754 |
|
|
|
|
| 763 |
// Generate subtitles for each highlight
|
| 764 |
highlights.forEach((highlight, i) => {
|
| 765 |
const textOptions = [
|
| 766 |
+
"This is the most exciting part of the video!",
|
| 767 |
+
"The best 1-minute segment from this content.",
|
| 768 |
+
"AI selected this as the most engaging moment.",
|
| 769 |
+
"Highlight reel of the most important content.",
|
| 770 |
+
"This 60-second clip contains the key takeaways."
|
| 771 |
];
|
| 772 |
|
| 773 |
subtitles.push({
|
|
|
|
| 784 |
clip.className = 'highlight-clip';
|
| 785 |
clip.style.left = `${(highlight.start / videoDuration) * 100}%`;
|
| 786 |
clip.style.width = `${((highlight.end - highlight.start) / videoDuration) * 100}%`;
|
| 787 |
+
clip.title = `1-min Highlight: ${formatTime(highlight.start)} - ${formatTime(highlight.end)}`;
|
| 788 |
+
clip.dataset.clipId = highlight.id;
|
| 789 |
waveform.appendChild(clip);
|
| 790 |
+
|
| 791 |
+
// Make highlight clip clickable
|
| 792 |
+
clip.addEventListener('click', (e) => {
|
| 793 |
+
e.stopPropagation();
|
| 794 |
+
seekToHighlight(highlight.start);
|
| 795 |
+
});
|
| 796 |
});
|
| 797 |
}
|
| 798 |
|
| 799 |
+
function seekToHighlight(time) {
|
| 800 |
+
if (videoType === 'youtube' && youtubePlayer) {
|
| 801 |
+
youtubePlayer.seekTo(time, true);
|
| 802 |
+
if (!isYouTubePlaying) {
|
| 803 |
+
youtubePlayer.playVideo();
|
| 804 |
+
}
|
| 805 |
+
} else if (videoType === 'file') {
|
| 806 |
+
videoPlayer.currentTime = time;
|
| 807 |
+
if (videoPlayer.paused) {
|
| 808 |
+
videoPlayer.play();
|
| 809 |
+
}
|
| 810 |
+
}
|
| 811 |
+
}
|
| 812 |
+
|
| 813 |
function displayHighlightClips() {
|
| 814 |
highlightClips.innerHTML = '';
|
| 815 |
highlightResults.innerHTML = '';
|
|
|
|
| 819 |
highlights.forEach((highlight, index) => {
|
| 820 |
// Create clip card for waveform section
|
| 821 |
const clipCard = document.createElement('div');
|
| 822 |
+
clipCard.className = 'bg-gray-700 rounded-lg p-4 flex items-start gap-4 hover:bg-gray-600 transition-colors cursor-pointer';
|
| 823 |
+
clipCard.addEventListener('click', () => seekToHighlight(highlight.start));
|
| 824 |
clipCard.innerHTML = `
|
| 825 |
<div class="bg-purple-600/20 w-12 h-12 rounded-full flex items-center justify-center flex-shrink-0">
|
| 826 |
<span class="text-xl font-bold">${index+1}</span>
|
| 827 |
</div>
|
| 828 |
<div>
|
| 829 |
<h4 class="font-semibold mb-1">${highlight.title}</h4>
|
| 830 |
+
<p class="text-sm text-gray-400 mb-2">${formatTime(highlight.start)} - ${formatTime(highlight.end)} (1 min)</p>
|
| 831 |
<p class="text-sm">${highlight.description}</p>
|
| 832 |
</div>
|
| 833 |
`;
|
|
|
|
| 835 |
|
| 836 |
// Create result card for results section
|
| 837 |
const resultCard = document.createElement('div');
|
| 838 |
+
resultCard.className = 'bg-gray-800 rounded-xl overflow-hidden border border-gray-700 clip-preview';
|
| 839 |
+
resultCard.setAttribute('data-clip-id', highlight.id);
|
| 840 |
resultCard.innerHTML = `
|
| 841 |
<div class="relative">
|
| 842 |
<div class="w-full bg-black aspect-video flex items-center justify-center relative">
|
| 843 |
${videoType === 'youtube' ?
|
| 844 |
+
`<img src="${highlight.previewImage}" alt="${titleBase}" class="w-full h-full object-cover">
|
| 845 |
+
<div class="clip-overlay absolute inset-0 flex items-center justify-center bg-black/50">
|
| 846 |
+
<div class="bg-white/20 rounded-full w-16 h-16 flex items-center justify-center backdrop-blur-sm">
|
| 847 |
<i class="fas fa-play text-white text-2xl"></i>
|
| 848 |
</div>
|
| 849 |
</div>` :
|
| 850 |
`<video class="w-full h-full" muted>
|
| 851 |
<source src="${videoBlobUrl}" type="video/mp4">
|
| 852 |
+
</video>
|
| 853 |
+
<div class="clip-overlay absolute inset-0 flex items-center justify-center bg-black/50">
|
| 854 |
+
<div class="bg-white/20 rounded-full w-16 h-16 flex items-center justify-center backdrop-blur-sm">
|
| 855 |
+
<i class="fas fa-play text-white text-2xl"></i>
|
| 856 |
+
</div>
|
| 857 |
+
</div>`}
|
| 858 |
</div>
|
| 859 |
<div class="absolute bottom-4 left-0 right-0 px-4">
|
| 860 |
<div class="bg-black/70 text-white rounded px-3 py-2 text-center max-w-md mx-auto">
|
| 861 |
+
${subtitles[index]?.text || '1-minute highlight'}
|
| 862 |
</div>
|
| 863 |
</div>
|
| 864 |
</div>
|
| 865 |
<div class="p-4">
|
| 866 |
<div class="flex justify-between items-start mb-2">
|
| 867 |
+
<div>
|
| 868 |
+
<h4 class="font-semibold text-lg">${titleBase}</h4>
|
| 869 |
+
<p class="text-sm text-gray-400">${highlight.title}</p>
|
| 870 |
+
</div>
|
| 871 |
+
<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}">
|
| 872 |
+
<i class="fas fa-download"></i> Download
|
| 873 |
</button>
|
| 874 |
</div>
|
| 875 |
+
<p class="text-sm text-gray-400 mb-2">${formatTime(highlight.start)} - ${formatTime(highlight.end)} (1 min)</p>
|
| 876 |
<p class="text-sm">${highlight.description}</p>
|
| 877 |
</div>
|
| 878 |
`;
|
| 879 |
highlightResults.appendChild(resultCard);
|
| 880 |
+
|
| 881 |
+
// Add click handler to preview the clip
|
| 882 |
+
const videoPreview = resultCard.querySelector('video, img');
|
| 883 |
+
videoPreview.parentElement.addEventListener('click', (e) => {
|
| 884 |
+
e.preventDefault();
|
| 885 |
+
seekToHighlight(highlight.start);
|
| 886 |
+
});
|
| 887 |
});
|
| 888 |
|
| 889 |
// Add event listeners to download buttons
|
| 890 |
document.querySelectorAll('.download-clip-btn').forEach(btn => {
|
| 891 |
+
btn.addEventListener('click', function(e) {
|
| 892 |
+
e.stopPropagation();
|
| 893 |
+
const clipId = this.getAttribute('data-clip-id');
|
| 894 |
+
downloadHighlight(clipId);
|
| 895 |
});
|
| 896 |
});
|
| 897 |
}
|
|
|
|
| 902 |
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
| 903 |
}
|
| 904 |
|
| 905 |
+
function downloadHighlight(clipId) {
|
| 906 |
+
const highlight = highlights.find(h => h.id === clipId);
|
| 907 |
+
if (!highlight) return;
|
| 908 |
|
| 909 |
+
// Show exporting status
|
| 910 |
+
exportStatus.classList.remove('hidden');
|
| 911 |
+
exportProgress.style.width = '0%';
|
| 912 |
+
exportPercent.textContent = '0%';
|
| 913 |
|
| 914 |
+
// Simulate export progress
|
| 915 |
+
let progress = 0;
|
| 916 |
+
const interval = setInterval(() => {
|
| 917 |
+
progress += Math.random() * 10;
|
| 918 |
+
if (progress >= 100) {
|
| 919 |
+
progress = 100;
|
| 920 |
+
clearInterval(interval);
|
| 921 |
+
|
| 922 |
+
// After a small delay, show completion
|
| 923 |
+
setTimeout(() => {
|
| 924 |
+
completeExport(highlight);
|
| 925 |
+
}, 500);
|
| 926 |
+
}
|
| 927 |
+
exportProgress.style.width = `${progress}%`;
|
| 928 |
+
exportPercent.textContent = `${Math.floor(progress)}%`;
|
| 929 |
+
}, 100);
|
| 930 |
}
|
| 931 |
|
| 932 |
+
function completeExport(highlight) {
|
| 933 |
+
exportProgress.style.width = '100%';
|
| 934 |
+
exportPercent.textContent = '100%';
|
| 935 |
+
|
| 936 |
+
setTimeout(() => {
|
| 937 |
+
// Create a dummy download link (in a real app, this would be your exported clip)
|
| 938 |
+
const dummyDownloadUrl = videoType === 'youtube' ?
|
| 939 |
+
`https://www.youtube.com/watch?v=${youtubeVideoId}&t=${Math.floor(highlight.start)}` :
|
| 940 |
+
videoBlobUrl;
|
| 941 |
+
|
| 942 |
+
const a = document.createElement('a');
|
| 943 |
+
a.href = dummyDownloadUrl;
|
| 944 |
+
a.download = `highlight_${formatTime(highlight.start)}_${formatTime(highlight.end)}.mp4`;
|
| 945 |
+
a.style.display = 'none';
|
| 946 |
+
document.body.appendChild(a);
|
| 947 |
+
a.click();
|
| 948 |
+
document.body.removeChild(a);
|
| 949 |
+
|
| 950 |
+
exportStatus.classList.add('hidden');
|
| 951 |
+
|
| 952 |
+
if (videoType === 'youtube') {
|
| 953 |
+
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.`);
|
| 954 |
+
} else {
|
| 955 |
+
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.`);
|
| 956 |
+
}
|
| 957 |
+
}, 500);
|
| 958 |
+
}
|
| 959 |
+
|
| 960 |
+
function handleDownloadAll() {
|
| 961 |
+
if (highlights.length === 0) return;
|
| 962 |
+
|
| 963 |
+
// Show exporting status
|
| 964 |
+
exportStatus.classList.remove('hidden');
|
| 965 |
+
exportProgress.style.width = '0%';
|
| 966 |
+
exportPercent.textContent = '0%';
|
| 967 |
+
|
| 968 |
+
// Simulate export progress for all clips
|
| 969 |
+
let progress = 0;
|
| 970 |
+
const interval = setInterval(() => {
|
| 971 |
+
progress += Math.random() * 5;
|
| 972 |
+
if (progress >= 100) {
|
| 973 |
+
progress = 100;
|
| 974 |
+
clearInterval(interval);
|
| 975 |
+
|
| 976 |
+
setTimeout(() => {
|
| 977 |
+
completeAllExport();
|
| 978 |
+
}, 500);
|
| 979 |
+
}
|
| 980 |
+
exportProgress.style.width = `${progress}%`;
|
| 981 |
+
exportPercent.textContent = `${Math.floor(progress)}%`;
|
| 982 |
+
}, 100);
|
| 983 |
+
}
|
| 984 |
+
|
| 985 |
+
function completeAllExport() {
|
| 986 |
+
exportProgress.style.width = '100%';
|
| 987 |
+
exportPercent.textContent = '100%';
|
| 988 |
+
|
| 989 |
+
setTimeout(() => {
|
| 990 |
+
// Create a dummy ZIP download (in a real app, this would package all clips)
|
| 991 |
+
let downloadUrl;
|
| 992 |
+
let downloadText;
|
| 993 |
+
|
| 994 |
+
if (videoType === 'youtube') {
|
| 995 |
+
downloadUrl = `https://www.youtube.com/watch?v=${youtubeVideoId}`;
|
| 996 |
+
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.`;
|
| 997 |
+
} else {
|
| 998 |
+
downloadUrl = videoBlobUrl;
|
| 999 |
+
downloadText = `In a real application, this would download all 1-minute highlights as a ZIP file. For demo purposes, it downloads the full video.`;
|
| 1000 |
+
}
|
| 1001 |
+
|
| 1002 |
+
const a = document.createElement('a');
|
| 1003 |
+
a.href = downloadUrl;
|
| 1004 |
+
a.download = `highlights_${videoType === 'youtube' ? youtubeTitle : 'your_video'}.zip`;
|
| 1005 |
+
a.style.display = 'none';
|
| 1006 |
+
document.body.appendChild(a);
|
| 1007 |
+
a.click();
|
| 1008 |
+
document.body.removeChild(a);
|
| 1009 |
+
|
| 1010 |
+
exportStatus.classList.add('hidden');
|
| 1011 |
+
alert(downloadText);
|
| 1012 |
+
}, 500);
|
| 1013 |
}
|
| 1014 |
});
|
| 1015 |
</script>
|