|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Free YouTube Caption Extractor</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 iframe { |
|
|
position: absolute; |
|
|
top: 0; |
|
|
left: 0; |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
} |
|
|
.caption-text { |
|
|
max-height: 300px; |
|
|
overflow-y: auto; |
|
|
} |
|
|
.caption-line:hover { |
|
|
background-color: #f0f0f0; |
|
|
cursor: pointer; |
|
|
} |
|
|
.caption-line.selected { |
|
|
background-color: #e2e8f0; |
|
|
font-weight: 500; |
|
|
} |
|
|
.fade-in { |
|
|
animation: fadeIn 0.5s ease-in-out; |
|
|
} |
|
|
@keyframes fadeIn { |
|
|
from { opacity: 0; transform: translateY(10px); } |
|
|
to { opacity: 1; transform: translateY(0); } |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body class="bg-gray-50 min-h-screen"> |
|
|
<div class="container mx-auto px-4 py-8 max-w-4xl"> |
|
|
|
|
|
<header class="text-center mb-8"> |
|
|
<h1 class="text-3xl md:text-4xl font-bold text-indigo-700 mb-2">YouTube Caption Extractor</h1> |
|
|
<p class="text-gray-600">Get captions/subtitles from any YouTube video for free</p> |
|
|
</header> |
|
|
|
|
|
|
|
|
<main> |
|
|
|
|
|
<div class="bg-white rounded-lg shadow-md p-6 mb-8"> |
|
|
<div class="flex flex-col md:flex-row gap-4"> |
|
|
<input |
|
|
type="text" |
|
|
id="youtube-url" |
|
|
placeholder="Paste YouTube video URL here (e.g. https://www.youtube.com/watch?v=...)" |
|
|
class="flex-grow px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500" |
|
|
> |
|
|
<button |
|
|
id="extract-btn" |
|
|
class="bg-indigo-600 hover:bg-indigo-700 text-white font-medium px-6 py-3 rounded-lg transition-colors flex items-center justify-center gap-2" |
|
|
> |
|
|
<i class="fas fa-play"></i> Extract Captions |
|
|
</button> |
|
|
</div> |
|
|
<div id="error-message" class="text-red-500 mt-2 text-sm hidden"></div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="video-section" class="hidden fade-in"> |
|
|
<div class="bg-white rounded-lg shadow-md overflow-hidden mb-6"> |
|
|
<div class="video-container"> |
|
|
<iframe id="video-player" frameborder="0" allowfullscreen></iframe> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="bg-white rounded-lg shadow-md p-6"> |
|
|
<div class="flex flex-col md:flex-row md:justify-between md:items-center mb-4 gap-4"> |
|
|
<h2 class="text-xl font-semibold text-gray-800">Video Captions</h2> |
|
|
<div class="flex flex-col sm:flex-row gap-2"> |
|
|
<select id="language-select" class="px-3 py-2 border border-gray-300 rounded-lg text-sm"> |
|
|
<option value="es">Spanish</option> |
|
|
<option value="fr">French</option> |
|
|
<option value="de">German</option> |
|
|
<option value="it">Italian</option> |
|
|
<option value="pt">Portuguese</option> |
|
|
<option value="ja">Japanese</option> |
|
|
<option value="ko">Korean</option> |
|
|
<option value="zh">Chinese</option> |
|
|
</select> |
|
|
<select id="format-select" class="px-3 py-2 border border-gray-300 rounded-lg text-sm"> |
|
|
<option value="plain">Plain Text</option> |
|
|
<option value="srt">SRT Format</option> |
|
|
<option value="tab">Tab Separated</option> |
|
|
</select> |
|
|
<button |
|
|
id="copy-all-btn" |
|
|
class="text-indigo-600 hover:text-indigo-800 flex items-center gap-1 text-sm whitespace-nowrap" |
|
|
> |
|
|
<i class="fas fa-copy"></i> Copy All |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="bg-gray-100 rounded-lg p-4 caption-text" id="captions-container"> |
|
|
<p class="text-gray-500 text-center py-8">Captions will appear here after extraction</p> |
|
|
</div> |
|
|
|
|
|
<div class="mt-4 flex justify-between items-center text-sm text-gray-500"> |
|
|
<div id="selected-count">0 captions selected</div> |
|
|
<button |
|
|
id="copy-selected-btn" |
|
|
class="text-indigo-600 hover:text-indigo-800 flex items-center gap-1 disabled:opacity-50" |
|
|
disabled |
|
|
> |
|
|
<i class="fas fa-copy"></i> Copy Selected |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</main> |
|
|
|
|
|
|
|
|
<section class="mt-12 bg-white rounded-lg shadow-md p-6"> |
|
|
<h2 class="text-xl font-semibold text-gray-800 mb-4">How It Works</h2> |
|
|
<div class="grid md:grid-cols-3 gap-6"> |
|
|
<div class="flex flex-col items-center text-center"> |
|
|
<div class="bg-indigo-100 w-12 h-12 rounded-full flex items-center justify-center mb-3"> |
|
|
<i class="fas fa-link text-indigo-600 text-xl"></i> |
|
|
</div> |
|
|
<h3 class="font-medium mb-1">1. Paste URL</h3> |
|
|
<p class="text-gray-600 text-sm">Enter any YouTube video URL in the box above</p> |
|
|
</div> |
|
|
<div class="flex flex-col items-center text-center"> |
|
|
<div class="bg-indigo-100 w-12 h-12 rounded-full flex items-center justify-center mb-3"> |
|
|
<i class="fas fa-play-circle text-indigo-600 text-xl"></i> |
|
|
</div> |
|
|
<h3 class="font-medium mb-1">2. Extract Captions</h3> |
|
|
<p class="text-gray-600 text-sm">Click the button to process the video</p> |
|
|
</div> |
|
|
<div class="flex flex-col items-center text-center"> |
|
|
<div class="bg-indigo-100 w-12 h-12 rounded-full flex items-center justify-center mb-3"> |
|
|
<i class="fas fa-copy text-indigo-600 text-xl"></i> |
|
|
</div> |
|
|
<h3 class="font-medium mb-1">3. Copy & Study</h3> |
|
|
<p class="text-gray-600 text-sm">Select and copy captions for your study needs</p> |
|
|
</div> |
|
|
</div> |
|
|
</section> |
|
|
|
|
|
|
|
|
<footer class="mt-12 text-center text-gray-500 text-sm"> |
|
|
<p>Free YouTube Caption Extractor - No registration required</p> |
|
|
<p class="mt-1">© 2023 All Rights Reserved</p> |
|
|
</footer> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="toast" class="fixed bottom-4 right-4 bg-gray-800 text-white px-4 py-2 rounded-lg shadow-lg hidden"> |
|
|
<div class="flex items-center gap-2"> |
|
|
<i class="fas fa-check-circle text-green-400"></i> |
|
|
<span id="toast-message">Copied to clipboard!</span> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
const youtubeUrlInput = document.getElementById('youtube-url'); |
|
|
const extractBtn = document.getElementById('extract-btn'); |
|
|
const videoSection = document.getElementById('video-section'); |
|
|
const videoPlayer = document.getElementById('video-player'); |
|
|
const captionsContainer = document.getElementById('captions-container'); |
|
|
const errorMessage = document.getElementById('error-message'); |
|
|
const copyAllBtn = document.getElementById('copy-all-btn'); |
|
|
const copySelectedBtn = document.getElementById('copy-selected-btn'); |
|
|
const selectedCount = document.getElementById('selected-count'); |
|
|
const toast = document.getElementById('toast'); |
|
|
const toastMessage = document.getElementById('toast-message'); |
|
|
|
|
|
let captions = []; |
|
|
let selectedCaptions = new Set(); |
|
|
let currentLanguage = 'es'; |
|
|
let currentFormat = 'plain'; |
|
|
|
|
|
|
|
|
function getVideoId(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 isValidYouTubeUrl(url) { |
|
|
return url.includes('youtube.com') || url.includes('youtu.be'); |
|
|
} |
|
|
|
|
|
|
|
|
function showError(message) { |
|
|
errorMessage.textContent = message; |
|
|
errorMessage.classList.remove('hidden'); |
|
|
} |
|
|
|
|
|
|
|
|
function hideError() { |
|
|
errorMessage.classList.add('hidden'); |
|
|
} |
|
|
|
|
|
|
|
|
function showToast(message) { |
|
|
toastMessage.textContent = message; |
|
|
toast.classList.remove('hidden'); |
|
|
setTimeout(() => { |
|
|
toast.classList.add('hidden'); |
|
|
}, 3000); |
|
|
} |
|
|
|
|
|
|
|
|
function extractCaptions(videoId) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return new Promise((resolve) => { |
|
|
|
|
|
setTimeout(() => { |
|
|
|
|
|
const mockCaptions = [ |
|
|
{ |
|
|
start: 0, |
|
|
duration: 5, |
|
|
original: "Hello and welcome to this tutorial.", |
|
|
translated: "Hola y bienvenidos a este tutorial." |
|
|
}, |
|
|
{ |
|
|
start: 5, |
|
|
duration: 4, |
|
|
original: "Today we'll learn about YouTube captions.", |
|
|
translated: "Hoy aprenderemos sobre los subtítulos de YouTube." |
|
|
}, |
|
|
{ |
|
|
start: 9, |
|
|
duration: 6, |
|
|
original: "Captions are important for accessibility.", |
|
|
translated: "Los subtítulos son importantes para la accesibilidad." |
|
|
}, |
|
|
{ |
|
|
start: 15, |
|
|
duration: 5, |
|
|
original: "They help people understand the content.", |
|
|
translated: "Ayudan a las personas a entender el contenido." |
|
|
}, |
|
|
{ |
|
|
start: 20, |
|
|
duration: 6, |
|
|
original: "You can use them for language learning too.", |
|
|
translated: "También puedes usarlos para aprender idiomas." |
|
|
} |
|
|
]; |
|
|
resolve(mockCaptions); |
|
|
}, 1000); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function displayCaptions(captionsData) { |
|
|
captions = captionsData; |
|
|
selectedCaptions.clear(); |
|
|
updateSelectedCount(); |
|
|
copySelectedBtn.disabled = true; |
|
|
|
|
|
if (captionsData.length === 0) { |
|
|
captionsContainer.innerHTML = '<p class="text-gray-500 text-center py-8">No captions available for this video</p>'; |
|
|
return; |
|
|
} |
|
|
|
|
|
let html = ''; |
|
|
captionsData.forEach((caption, index) => { |
|
|
html += ` |
|
|
<div class="caption-line py-2 px-3 rounded mb-1" data-index="${index}"> |
|
|
<div class="font-medium mb-1">${caption.original}</div> |
|
|
<div class="text-gray-600 text-sm">${caption.translated}</div> |
|
|
</div> |
|
|
`; |
|
|
}); |
|
|
|
|
|
captionsContainer.innerHTML = html; |
|
|
|
|
|
|
|
|
document.querySelectorAll('.caption-line').forEach(line => { |
|
|
line.addEventListener('click', function() { |
|
|
const index = parseInt(this.getAttribute('data-index')); |
|
|
|
|
|
if (selectedCaptions.has(index)) { |
|
|
selectedCaptions.delete(index); |
|
|
this.classList.remove('selected'); |
|
|
} else { |
|
|
selectedCaptions.add(index); |
|
|
this.classList.add('selected'); |
|
|
} |
|
|
|
|
|
updateSelectedCount(); |
|
|
copySelectedBtn.disabled = selectedCaptions.size === 0; |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function updateSelectedCount() { |
|
|
const count = selectedCaptions.size; |
|
|
selectedCount.textContent = `${count} caption${count !== 1 ? 's' : ''} selected`; |
|
|
} |
|
|
|
|
|
|
|
|
document.getElementById('language-select').addEventListener('change', function() { |
|
|
currentLanguage = this.value; |
|
|
|
|
|
|
|
|
displayCaptions(captions); |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('format-select').addEventListener('change', function() { |
|
|
currentFormat = this.value; |
|
|
}); |
|
|
|
|
|
|
|
|
function formatCaptionText(original, translated) { |
|
|
switch(currentFormat) { |
|
|
case 'srt': |
|
|
return `${original}\n${translated}`; |
|
|
case 'tab': |
|
|
return `${original}\t${translated}`; |
|
|
default: |
|
|
return `${original}\n${translated}`; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
copyAllBtn.addEventListener('click', function() { |
|
|
if (captions.length === 0) return; |
|
|
|
|
|
const allText = captions.map(c => formatCaptionText(c.original, c.translated)).join('\n\n'); |
|
|
navigator.clipboard.writeText(allText).then(() => { |
|
|
showToast('All captions copied to clipboard!'); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
copySelectedBtn.addEventListener('click', function() { |
|
|
if (selectedCaptions.size === 0) return; |
|
|
|
|
|
const selectedText = Array.from(selectedCaptions) |
|
|
.sort((a, b) => a - b) |
|
|
.map(index => formatCaptionText(captions[index].original, captions[index].translated)) |
|
|
.join(currentFormat === 'tab' ? '\n' : '\n\n'); |
|
|
|
|
|
navigator.clipboard.writeText(selectedText).then(() => { |
|
|
showToast('Selected captions copied to clipboard!'); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
extractBtn.addEventListener('click', function() { |
|
|
const url = youtubeUrlInput.value.trim(); |
|
|
|
|
|
if (!url) { |
|
|
showError('Please enter a YouTube URL'); |
|
|
return; |
|
|
} |
|
|
|
|
|
if (!isValidYouTubeUrl(url)) { |
|
|
showError('Please enter a valid YouTube URL'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const videoId = getVideoId(url); |
|
|
if (!videoId) { |
|
|
showError('Could not extract video ID from URL'); |
|
|
return; |
|
|
} |
|
|
|
|
|
hideError(); |
|
|
extractBtn.disabled = true; |
|
|
extractBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Extracting...'; |
|
|
|
|
|
|
|
|
videoPlayer.src = `https://www.youtube.com/embed/${videoId}?cc_load_policy=1`; |
|
|
videoSection.classList.remove('hidden'); |
|
|
|
|
|
|
|
|
extractCaptions(videoId) |
|
|
.then(captions => { |
|
|
displayCaptions(captions); |
|
|
extractBtn.disabled = false; |
|
|
extractBtn.innerHTML = '<i class="fas fa-play"></i> Extract Captions'; |
|
|
}) |
|
|
.catch(error => { |
|
|
console.error('Error extracting captions:', error); |
|
|
captionsContainer.innerHTML = '<p class="text-red-500 text-center py-8">Error extracting captions. Please try another video.</p>'; |
|
|
extractBtn.disabled = false; |
|
|
extractBtn.innerHTML = '<i class="fas fa-play"></i> Extract Captions'; |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
youtubeUrlInput.addEventListener('keypress', function(e) { |
|
|
if (e.key === 'Enter') { |
|
|
extractBtn.click(); |
|
|
} |
|
|
}); |
|
|
}); |
|
|
</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=dimchr/captions" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
|
</html> |