Spaces:
Running
Running
| // Video page functionality | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const playerElement = document.getElementById('youtube-player'); | |
| const searchInput = document.getElementById('search-input'); | |
| const searchButton = document.getElementById('search-button'); | |
| const transcriptContainer = document.getElementById('transcript-container'); | |
| const loadingIndicator = document.getElementById('loading'); | |
| const toggleTranscriptButton = document.getElementById('toggle-transcript'); | |
| let transcriptSegments = []; | |
| let ytPlayer = null; | |
| let isProcessingUrl = false; | |
| // Check if there's a search query or timestamp in the URL | |
| const urlParams = new URLSearchParams(window.location.search); | |
| const searchQuery = urlParams.get('q'); | |
| const processingUrl = urlParams.get('processing'); | |
| const startTime = urlParams.get('t'); | |
| // Format time to display as HH:MM:SS | |
| function formatTime(seconds) { | |
| const hours = Math.floor(seconds / 3600); | |
| const mins = Math.floor((seconds % 3600) / 60); | |
| const secs = Math.floor(seconds % 60); | |
| if (hours > 0) { | |
| return `${hours}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; | |
| } else { | |
| return `${mins}:${secs.toString().padStart(2, '0')}`; | |
| } | |
| } | |
| // Handle error display | |
| function handleError(error) { | |
| console.error(error); | |
| return `<div class="alert alert-error">Error: ${error.message}</div>`; | |
| } | |
| // Initialize YouTube iframe API | |
| function initYouTubePlayer() { | |
| // Get the existing iframe | |
| const iframeId = playerElement.getAttribute('id'); | |
| // Load the YouTube iframe API if it's not already loaded | |
| if (!window.YT) { | |
| const tag = document.createElement('script'); | |
| tag.src = 'https://www.youtube.com/iframe_api'; | |
| const firstScriptTag = document.getElementsByTagName('script')[0]; | |
| firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); | |
| window.onYouTubeIframeAPIReady = function() { | |
| createYouTubePlayer(iframeId); | |
| }; | |
| } else { | |
| createYouTubePlayer(iframeId); | |
| } | |
| } | |
| // Create YouTube player object | |
| function createYouTubePlayer(iframeId) { | |
| ytPlayer = new YT.Player(iframeId, { | |
| events: { | |
| 'onReady': onPlayerReady | |
| } | |
| }); | |
| } | |
| // When player is ready | |
| function onPlayerReady(event) { | |
| console.log('Player ready'); | |
| // Seeking will be handled by the dedicated timestamp handler code at the bottom | |
| } | |
| // Load transcript segments | |
| function loadTranscript() { | |
| transcriptContainer.innerHTML = '<div class="flex justify-center my-4"><span class="loading loading-spinner loading-md"></span><span class="ml-2">Loading transcript...</span></div>'; | |
| // Check if video ID is valid before making API call | |
| if (!videoId || videoId === 'undefined' || videoId === 'null') { | |
| transcriptContainer.innerHTML = ` | |
| <div class="alert alert-error"> | |
| <div> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> | |
| <span>Invalid video ID. Please return to the home page and select a valid video.</span> | |
| </div> | |
| </div> | |
| `; | |
| return; | |
| } | |
| fetch(`/api/video/segments/${videoId}`) | |
| .then(response => { | |
| if (!response.ok) { | |
| throw new Error('Failed to load transcript: ' + response.status); | |
| } | |
| return response.json(); | |
| }) | |
| .then(segments => { | |
| transcriptSegments = segments; | |
| if (!segments || segments.length === 0) { | |
| transcriptContainer.innerHTML = ` | |
| <div class="alert alert-info"> | |
| <div> | |
| <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg> | |
| <span>No transcript available for this video. Try processing the video first from the home page.</span> | |
| </div> | |
| </div> | |
| `; | |
| } else { | |
| displayTranscript(segments); | |
| // If we have a timestamp in the URL, highlight the relevant segment | |
| if (startTime) { | |
| const timeInSeconds = parseFloat(startTime); | |
| if (!isNaN(timeInSeconds)) { | |
| // Find segment containing this time | |
| setTimeout(() => { | |
| highlightSegment(timeInSeconds); | |
| }, 500); // Short delay to ensure segments are rendered | |
| } | |
| } | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Error loading transcript:', error); | |
| transcriptContainer.innerHTML = ` | |
| <div class="alert alert-error"> | |
| <div> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> | |
| <span>Error loading transcript: ${error.message}</span> | |
| </div> | |
| </div> | |
| <p class="mt-4">This may happen if:</p> | |
| <ul class="list-disc ml-8 mt-2"> | |
| <li>The video hasn't been processed yet</li> | |
| <li>The video ID is incorrect</li> | |
| <li>The server is experiencing issues</li> | |
| </ul> | |
| <p class="mt-4">Try processing this video from the home page first.</p> | |
| `; | |
| }); | |
| } | |
| // Display transcript segments | |
| function displayTranscript(segments) { | |
| const html = segments.map((segment, index) => { | |
| const formattedTime = formatTime(segment.start); | |
| return ` | |
| <div class="transcript-segment" data-start="${segment.start}" data-end="${segment.end}" data-index="${index}"> | |
| <span class="timestamp">${formattedTime}</span> | |
| <span class="segment-text">${segment.text}</span> | |
| </div> | |
| `; | |
| }).join(''); | |
| transcriptContainer.innerHTML = html; | |
| // Add click handlers to segments | |
| document.querySelectorAll('.transcript-segment').forEach(segment => { | |
| segment.addEventListener('click', () => { | |
| const startTime = parseFloat(segment.dataset.start); | |
| seekToTime(startTime); | |
| }); | |
| }); | |
| } | |
| // Seek to specific time in the video | |
| function seekToTime(seconds) { | |
| console.log('Seeking to time:', seconds); | |
| if (ytPlayer && typeof ytPlayer.seekTo === 'function') { | |
| try { | |
| // Ensure seconds is a number | |
| seconds = parseFloat(seconds); | |
| if (isNaN(seconds)) { | |
| console.error('Invalid seconds value:', seconds); | |
| return; | |
| } | |
| // Seek to time | |
| ytPlayer.seekTo(seconds, true); | |
| // Try to play the video (may be blocked by browser autoplay policies) | |
| try { | |
| ytPlayer.playVideo(); | |
| } catch (e) { | |
| console.warn('Could not autoplay video:', e); | |
| } | |
| // Highlight the current segment | |
| setTimeout(() => { | |
| highlightSegment(seconds); | |
| }, 300); // Short delay to ensure seek completes first | |
| } catch (error) { | |
| console.error('Error seeking to time:', error); | |
| } | |
| } else { | |
| console.error('YouTube player is not ready yet or seekTo method is not available'); | |
| // Queue the seek operation for when the player becomes available | |
| console.log('Queueing seek operation for later...'); | |
| setTimeout(() => { | |
| if (ytPlayer && typeof ytPlayer.seekTo === 'function') { | |
| console.log('Player now ready, executing queued seek'); | |
| seekToTime(seconds); | |
| } | |
| }, 1000); // Try again in 1 second | |
| } | |
| } | |
| // Highlight segment containing the current time | |
| function highlightSegment(time) { | |
| // Remove highlight from all segments | |
| document.querySelectorAll('.transcript-segment').forEach(segment => { | |
| segment.classList.remove('highlight'); | |
| }); | |
| // Wait until segments are available in the DOM | |
| if (document.querySelectorAll('.transcript-segment').length === 0) { | |
| console.log('No transcript segments found, waiting...'); | |
| // Retry after a short delay to allow transcript to load | |
| setTimeout(() => highlightSegment(time), 500); | |
| return; | |
| } | |
| // Find the segment containing current time | |
| // Need to find by approximate match since floating point exact matches may not work | |
| const segments = document.querySelectorAll('.transcript-segment'); | |
| let currentSegment = null; | |
| for (const segment of segments) { | |
| const start = parseFloat(segment.dataset.start); | |
| const end = parseFloat(segment.dataset.end); | |
| if (time >= start && time <= end) { | |
| currentSegment = segment; | |
| break; | |
| } | |
| } | |
| // If exact time match not found, find the closest segment by time | |
| if (!currentSegment && segments.length > 0) { | |
| // First try exact match | |
| const exactMatch = document.querySelector(`.transcript-segment[data-start="${time}"]`); | |
| if (exactMatch) { | |
| currentSegment = exactMatch; | |
| } else { | |
| // Find closest segment | |
| let closestSegment = segments[0]; | |
| let closestDistance = Math.abs(parseFloat(closestSegment.dataset.start) - time); | |
| segments.forEach(segment => { | |
| const distance = Math.abs(parseFloat(segment.dataset.start) - time); | |
| if (distance < closestDistance) { | |
| closestDistance = distance; | |
| closestSegment = segment; | |
| } | |
| }); | |
| currentSegment = closestSegment; | |
| } | |
| } | |
| if (currentSegment) { | |
| currentSegment.classList.add('highlight'); | |
| // Ensure the segment is visible in the transcript container | |
| currentSegment.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| } | |
| } | |
| // Search functionality | |
| searchButton.addEventListener('click', performSearch); | |
| searchInput.addEventListener('keypress', e => { | |
| if (e.key === 'Enter') performSearch(); | |
| }); | |
| function performSearch() { | |
| const query = searchInput.value.trim(); | |
| if (!query) { | |
| transcriptContainer.innerHTML = '<div class="alert alert-warning">Please enter a search query</div>'; | |
| return; | |
| } | |
| // Validate video ID before searching | |
| if (!videoId || videoId === 'undefined' || videoId === 'null') { | |
| transcriptContainer.innerHTML = ` | |
| <div class="alert alert-error"> | |
| <div> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> | |
| <span>Invalid video ID. Please return to the home page and select a valid video.</span> | |
| </div> | |
| </div> | |
| `; | |
| return; | |
| } | |
| // Show loading indicator | |
| loadingIndicator.classList.remove('hidden'); | |
| // Send search request | |
| fetch(`/api/video/search?query=${encodeURIComponent(query)}&video_id=${videoId}`) | |
| .then(response => { | |
| if (!response.ok) { | |
| throw new Error('Search failed'); | |
| } | |
| return response.json(); | |
| }) | |
| .then(results => { | |
| // Hide loading indicator | |
| loadingIndicator.classList.add('hidden'); | |
| if (results.length === 0) { | |
| // Show "no results" message in transcript container | |
| transcriptContainer.innerHTML = ` | |
| <div role="alert" class="alert alert-info"> | |
| <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path> | |
| </svg> | |
| <span>No results found for "${query}". <a href="#" id="reset-search" class="link link-primary">Show all transcript</a></span> | |
| </div>`; | |
| // Add click handler to reset search link | |
| document.getElementById('reset-search').addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| resetTranscriptFilter(); | |
| displayTranscript(transcriptSegments); | |
| }); | |
| return; | |
| } | |
| // Display search results as filtered transcript | |
| filterTranscript(results); | |
| // Add a header with search info and reset option | |
| const searchInfoHeader = document.createElement('div'); | |
| searchInfoHeader.className = 'mb-4 flex justify-between items-center'; | |
| searchInfoHeader.innerHTML = ` | |
| <div class="badge badge-accent">${results.length} results for "${query}"</div> | |
| <a href="#" id="reset-search" class="link link-primary text-sm">Show all transcript</a> | |
| `; | |
| // Insert the header before transcript segments | |
| transcriptContainer.insertBefore(searchInfoHeader, transcriptContainer.firstChild); | |
| // Add click handler to reset search link | |
| document.getElementById('reset-search').addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| resetTranscriptFilter(); | |
| displayTranscript(transcriptSegments); | |
| }); | |
| }) | |
| .catch(error => { | |
| // Hide loading indicator | |
| loadingIndicator.classList.add('hidden'); | |
| // Show error | |
| transcriptContainer.innerHTML = handleError(error); | |
| }); | |
| } | |
| // Filter transcript to show only matching segments | |
| function filterTranscript(results) { | |
| // Create a highlighted version of the transcript with only matching segments | |
| const html = results.map(result => { | |
| const segment = result.segment; | |
| const formattedTime = formatTime(segment.start); | |
| const score = (result.score * 100).toFixed(0); | |
| const index = transcriptSegments.findIndex(s => s.segment_id === segment.segment_id); | |
| return ` | |
| <div class="transcript-segment search-result" data-start="${segment.start}" data-end="${segment.end}" data-index="${index}"> | |
| <div class="flex justify-between items-center"> | |
| <span class="timestamp">${formattedTime}</span> | |
| <div class="badge badge-primary">${score}% match</div> | |
| </div> | |
| <span class="segment-text mt-1">${segment.text}</span> | |
| </div> | |
| `; | |
| }).join(''); | |
| // Replace transcript with filtered results | |
| transcriptContainer.innerHTML = html; | |
| // Add click handlers to segments | |
| document.querySelectorAll('.transcript-segment').forEach(segment => { | |
| segment.addEventListener('click', () => { | |
| const startTime = parseFloat(segment.dataset.start); | |
| seekToTime(startTime); | |
| }); | |
| }); | |
| } | |
| // Transcript is always visible - toggle functionality removed | |
| // Reset transcript filter to show all segments | |
| function resetTranscriptFilter() { | |
| searchInput.value = ''; | |
| } | |
| // Show processing indicator if URL was just processed | |
| function showProcessingIndicator() { | |
| if (processingUrl === 'true') { | |
| isProcessingUrl = true; | |
| transcriptContainer.innerHTML = ` | |
| <div class="flex items-center justify-center my-4"> | |
| <span class="loading loading-spinner loading-md text-primary"></span> | |
| <span class="ml-2">Processing video from URL... This may take a few moments</span> | |
| </div> | |
| `; | |
| // Check for segments every second | |
| const processingInterval = setInterval(() => { | |
| // Validate video ID before making API call | |
| if (!videoId || videoId === 'undefined' || videoId === 'null') { | |
| clearInterval(processingInterval); | |
| transcriptContainer.innerHTML = ` | |
| <div class="alert alert-error"> | |
| <div> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> | |
| <span>Invalid video ID. Please return to the home page and select a valid video.</span> | |
| </div> | |
| </div> | |
| `; | |
| return; | |
| } | |
| fetch(`/api/video/segments/${videoId}`) | |
| .then(response => { | |
| if (!response.ok) { | |
| return null; | |
| } | |
| return response.json(); | |
| }) | |
| .then(segments => { | |
| if (segments && segments.length > 0) { | |
| clearInterval(processingInterval); | |
| isProcessingUrl = false; | |
| loadTranscript(); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Error checking segments:', error); | |
| }); | |
| }, 2000); | |
| // Set timeout to stop checking after 2 minutes | |
| setTimeout(() => { | |
| clearInterval(processingInterval); | |
| if (isProcessingUrl) { | |
| transcriptContainer.innerHTML = ` | |
| <div class="alert alert-warning"> | |
| <div> | |
| <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg> | |
| <span>Processing is taking longer than expected. Refresh the page to check progress.</span> | |
| </div> | |
| </div> | |
| `; | |
| isProcessingUrl = false; | |
| } | |
| }, 120000); | |
| return true; | |
| } | |
| return false; | |
| } | |
| // Initialize | |
| initYouTubePlayer(); | |
| // Show processing indicator or load transcript | |
| if (!showProcessingIndicator()) { | |
| loadTranscript(); | |
| } | |
| // If there's a search query in the URL, apply it after transcript loads | |
| if (searchQuery) { | |
| const checkTranscriptInterval = setInterval(() => { | |
| if (transcriptSegments.length > 0) { | |
| clearInterval(checkTranscriptInterval); | |
| // Set the search input value and trigger search | |
| searchInput.value = searchQuery; | |
| performSearch(); | |
| } | |
| }, 500); | |
| // Set timeout to stop checking after 10 seconds | |
| setTimeout(() => clearInterval(checkTranscriptInterval), 10000); | |
| } | |
| // If there's a timestamp in the URL, ensure it will be seeked to after transcript loads | |
| if (startTime) { // Handle timestamp regardless of search query | |
| let timeInSeconds = parseFloat(startTime); | |
| // If parsing fails, try to parse as HH:MM:SS format | |
| if (isNaN(timeInSeconds) && typeof startTime === 'string') { | |
| // Try to parse HH:MM:SS or MM:SS format | |
| const timeParts = startTime.split(':').map(part => parseInt(part, 10)); | |
| if (timeParts.length === 3) { | |
| // HH:MM:SS format | |
| timeInSeconds = timeParts[0] * 3600 + timeParts[1] * 60 + timeParts[2]; | |
| } else if (timeParts.length === 2) { | |
| // MM:SS format | |
| timeInSeconds = timeParts[0] * 60 + timeParts[1]; | |
| } | |
| } | |
| if (!isNaN(timeInSeconds)) { | |
| console.log('Will seek to timestamp:', timeInSeconds, 'seconds'); | |
| // Try immediately if player is ready | |
| if (ytPlayer && typeof ytPlayer.seekTo === 'function') { | |
| console.log('YouTube player is ready, seeking now'); | |
| seekToTime(timeInSeconds); | |
| // Also try to play the video as a fallback if autoplay doesn't work | |
| try { | |
| ytPlayer.playVideo(); | |
| // Unmute the video after a short delay | |
| setTimeout(() => { | |
| try { | |
| ytPlayer.unMute(); | |
| // Set volume to a reasonable level | |
| ytPlayer.setVolume(80); | |
| } catch (e) { | |
| console.warn('Could not unmute video:', e); | |
| } | |
| }, 1000); | |
| } catch (e) { | |
| console.warn('Could not autoplay video:', e); | |
| } | |
| } | |
| // Also set up a backup interval to ensure we seek once everything is ready | |
| const checkReadyInterval = setInterval(() => { | |
| if (ytPlayer && typeof ytPlayer.seekTo === 'function') { | |
| if (transcriptSegments.length > 0) { | |
| clearInterval(checkReadyInterval); | |
| console.log('Everything loaded, seeking to timestamp:', timeInSeconds); | |
| seekToTime(timeInSeconds); | |
| // Also try to play the video | |
| try { | |
| ytPlayer.playVideo(); | |
| // Unmute the video after a short delay | |
| setTimeout(() => { | |
| try { | |
| ytPlayer.unMute(); | |
| ytPlayer.setVolume(80); | |
| } catch (e) { | |
| console.warn('Could not unmute video after delay:', e); | |
| } | |
| }, 1000); | |
| } catch (e) { | |
| console.warn('Could not autoplay video after delay:', e); | |
| } | |
| } else { | |
| console.log('Waiting for transcript segments to load...'); | |
| } | |
| } else { | |
| console.log('Waiting for YouTube player to be ready...'); | |
| } | |
| }, 500); | |
| // Set timeout to stop checking after 10 seconds | |
| setTimeout(() => { | |
| clearInterval(checkReadyInterval); | |
| // Final attempt | |
| if (ytPlayer && typeof ytPlayer.seekTo === 'function') { | |
| console.log('Final attempt to seek to:', timeInSeconds); | |
| seekToTime(timeInSeconds); | |
| // One final attempt to play | |
| try { | |
| ytPlayer.playVideo(); | |
| // Unmute the video after a short delay | |
| setTimeout(() => { | |
| try { | |
| ytPlayer.unMute(); | |
| ytPlayer.setVolume(80); | |
| } catch (e) { | |
| console.warn('Could not unmute video on final attempt:', e); | |
| } | |
| }, 1000); | |
| } catch (e) { | |
| console.warn('Could not autoplay video on final attempt:', e); | |
| } | |
| } | |
| }, 10000); | |
| } else { | |
| console.warn('Could not parse timestamp from URL:', startTime); | |
| } | |
| } | |
| }); | |