| <!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> |