Transcript Hooks
Custom hooks for transcript fetching and YouTube player control
Transcript Hooks
The transcript feature uses three custom hooks to handle API fetching, YouTube player integration, and download functionality. Each hook encapsulates specific logic and interacts with the transcript store.
useFetchTranscript
This hook handles fetching video details and transcript data from the API endpoints. It manages loading states, error handling, rate limiting, and data processing.
Implementation
import { useToast } from '@/hooks/use-toast';
import { useTranscribeStore } from '../store/transcribe-store';
import type { TranscriptSegment } from '../store/transcribe-store';
interface TranscriptResponse {
transcript: {
segments?: Array<{
text: string;
startTime: string;
endTime: string;
}>;
fullTranscript?: string;
};
}
export const useFetchTranscript = () => {
const { toast } = useToast();
const {
videoUrl,
setVideoDetails,
setTranscriptData,
setIsLoading,
setIsSuccess
} = useTranscribeStore();
const fetchTranscript = async () => {
if (!videoUrl) {
toast({
title: 'Error',
description: 'Please enter a YouTube video URL',
variant: 'destructive'
});
return;
}
setIsLoading(true);
setIsSuccess(false);
try {
// Fetch video details
const videoResponse = await fetch('/api/videoDetail', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ videoUrl })
});
const videoData = await videoResponse.json();
if (!videoResponse.ok) {
throw new Error(videoData.error || 'Failed to fetch video details');
}
if (videoData.video) {
setVideoDetails(videoData.video);
}
// Fetch transcript
const transcriptResponse = await fetch('/api/transcribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ videoUrl })
});
if (transcriptResponse.status === 429) {
const data = await transcriptResponse.json();
toast({
title: 'Rate Limit Exceeded',
description: `Too many requests. Please try again in ${Math.ceil((data.reset - Date.now()) / 1000)} seconds.`,
variant: 'destructive'
});
setIsLoading(false);
return;
}
if (!transcriptResponse.ok) {
const errorData = await transcriptResponse.json();
throw new Error(errorData.error || 'Failed to fetch transcript');
}
const transcriptData: TranscriptResponse =
await transcriptResponse.json();
processTranscriptData(transcriptData.transcript);
setIsSuccess(true);
setTimeout(() => setIsSuccess(false), 4000);
} catch (error: any) {
console.error('Error fetching data:', error);
setTranscriptData([]);
toast({
title: 'Error',
description: error.message || 'Failed to fetch transcript',
variant: 'destructive'
});
} finally {
setIsLoading(false);
}
};
const processTranscriptData = (transcript: any) => {
if (!transcript) return;
let formattedTranscript: TranscriptSegment[] = [];
if (transcript.segments) {
formattedTranscript = transcript.segments.map((segment: any) => ({
text: segment.text,
startTime: segment.startTime,
endTime: segment.endTime
}));
} else if (transcript.fullTranscript) {
formattedTranscript = [
{
text: transcript.fullTranscript,
startTime: '0:00',
endTime: '0:00'
}
];
}
setTranscriptData(formattedTranscript);
};
return { fetchTranscript };
};How It Works
Step 1: Validation
- Checks if
videoUrlexists in store - Shows error toast if URL is empty
- Returns early to prevent API calls
Step 2: Loading State
- Sets
isLoadingtotruefor UI feedback - Resets
isSuccesstofalseto clear previous success state
Step 3: Fetch Video Details
- POSTs to
/api/videoDetailwith the video URL - Parses JSON response
- Checks for HTTP errors and throws if failed
- Updates store with
setVideoDetails()if successful
Step 4: Fetch Transcript
- POSTs to
/api/transcribewith the video URL - Handles rate limiting (429 status)
- Shows toast with countdown timer
- Returns early without throwing error
- Checks for HTTP errors and throws if failed
- Processes transcript data using helper function
Step 5: Process Transcript Data
- Handles two response formats:
- Segments Format: Array of transcript segments with timestamps
- Full Transcript Format: Single text block (fallback)
- Maps segments to
TranscriptSegment[]type - For full transcript, creates single segment with 0:00 timestamps
- Updates store with
setTranscriptData()
Step 6: Success Handling
- Sets
isSuccesstotruefor button animation - Uses
setTimeoutto auto-reset after 4 seconds
Step 7: Error Handling
- Catches any errors from API calls
- Logs error to console
- Clears transcript data
- Shows error toast with message
Step 8: Cleanup
- Always sets
isLoadingtofalsein finally block - Ensures loading state is cleared regardless of success/failure
useYoutubePlayer
This hook manages YouTube IFrame API integration. It loads the YouTube API script, initializes the player, and provides seek controls.
Implementation
import { useEffect, useState, useCallback } from 'react';
declare global {
interface Window {
YT: any;
onYouTubeIframeAPIReady: (() => void) | undefined;
}
}
export const useYoutubePlayer = () => {
const [isApiReady, setIsApiReady] = useState(false);
const [isScriptLoaded, setIsScriptLoaded] = useState(false);
useEffect(() => {
// Check if API is already loaded
if (window.YT && window.YT.Player) {
setIsApiReady(true);
setIsScriptLoaded(true);
return;
}
// Check if script is already in the DOM
const existingScript = document.querySelector(
'script[src="https://www.youtube.com/iframe_api"]'
);
if (existingScript) {
setIsScriptLoaded(true);
// Wait for the API to be ready
const checkInterval = setInterval(() => {
if (window.YT && window.YT.Player) {
setIsApiReady(true);
clearInterval(checkInterval);
}
}, 100);
return () => clearInterval(checkInterval);
}
// Load the YouTube IFrame API script
window.onYouTubeIframeAPIReady = () => {
setIsApiReady(true);
};
const tag = document.createElement('script');
tag.src = 'https://www.youtube.com/iframe_api';
tag.async = true;
tag.onload = () => setIsScriptLoaded(true);
const firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode?.insertBefore(tag, firstScriptTag);
// No cleanup needed - keep the API loaded for other components
}, []);
const initializePlayer = useCallback(
(elementId: string, videoId: string, onReady?: (player: any) => void) => {
if (!isApiReady || !window.YT) return null;
try {
const player = new window.YT.Player(elementId, {
videoId: videoId,
events: {
onReady: (event: any) => {
if (onReady) onReady(event.target);
},
onError: (event: any) => {
console.error('YouTube player error:', event.data);
}
}
});
return player;
} catch (error) {
console.error('Error initializing YouTube player:', error);
return null;
}
},
[isApiReady]
);
const seekTo = useCallback((player: any, timeInSeconds: number) => {
if (player && typeof player.seekTo === 'function') {
player.seekTo(timeInSeconds, true);
if (typeof player.playVideo === 'function') {
player.playVideo();
}
}
}, []);
return {
isApiReady,
isScriptLoaded,
initializePlayer,
seekTo
};
};How It Works
Loading YouTube IFrame API
The useEffect hook handles three scenarios:
-
API Already Loaded: If
window.YT.Playerexists, immediately set both flags totrue -
Script Already in DOM: If script tag exists but API not ready
- Set
isScriptLoadedtotrue - Poll every 100ms until
window.YT.Playeris available - Clear interval when ready
- Set
-
Fresh Load: If script doesn't exist
- Define
window.onYouTubeIframeAPIReadycallback - Create script tag with src
https://www.youtube.com/iframe_api - Set async attribute for non-blocking load
- Insert before first script tag in document
- Set
isScriptLoadedwhen script loads - YouTube calls the callback when API is ready
- Define
initializePlayer(elementId, videoId, onReady)
Creates a YouTube player instance:
-
Parameters:
elementId: DOM element ID where player will be mountedvideoId: YouTube video ID to loadonReady: Optional callback receiving player instance
-
Process:
- Checks if API is ready and
window.YTexists - Creates new
YT.Playerinstance - Configures
videoIdfor the video to play - Sets up event handlers:
onReady: Calls provided callback with player instanceonError: Logs error data to console
- Returns player instance or
nullif error
- Checks if API is ready and
-
Error Handling: Wrapped in try-catch, logs errors and returns
null
seekTo(player, timeInSeconds)
Seeks video to specific timestamp and starts playback:
-
Parameters:
player: YouTube player instancetimeInSeconds: Target time in seconds
-
Process:
- Validates player exists and has
seekTomethod - Calls
player.seekTo(time, true)- second parameter allows seeking ahead - Calls
player.playVideo()to auto-play after seek
- Validates player exists and has
-
Safety: Checks method existence before calling to prevent errors
Return Values
isApiReady: True when YouTube API is loaded and readyisScriptLoaded: True when script tag is loaded (API may still be initializing)initializePlayer: Function to create player instancesseekTo: Function to seek and play video at timestamp
useTranscriptDownload
This hook provides functions to download transcripts in three formats: full text, timestamped text, and SRT subtitles.
Implementation
import { useTranscribeStore } from '../store/transcribe-store';
export const useTranscriptDownload = () => {
const { transcriptData, videoDetails } = useTranscribeStore();
const downloadFullTranscript = () => {
const fullTranscript = transcriptData.map((entry) => entry.text).join(' ');
downloadFile(
fullTranscript,
`transcript-${videoDetails?.title || 'video'}.txt`
);
};
const downloadTimestampedTranscript = () => {
const formattedTranscript = transcriptData
.map(
(entry) => `[${entry.startTime} - ${entry.endTime}]\n${entry.text}\n`
)
.join('\n');
downloadFile(
formattedTranscript,
`timestamped-transcript-${videoDetails?.title || 'video'}.txt`
);
};
const downloadSrtSubtitles = () => {
const srtContent = transcriptData
.map((entry, index) => {
const startTime = convertToSrtTime(entry.startTime);
const endTime = convertToSrtTime(entry.endTime);
return `${index + 1}\n${startTime} --> ${endTime}\n${entry.text}\n`;
})
.join('\n');
downloadFile(srtContent, `subtitles-${videoDetails?.title || 'video'}.srt`);
};
const convertToSrtTime = (timeStr: string): string => {
const [minutes, seconds] = timeStr.split(':').map(Number);
const totalSeconds = minutes * 60 + seconds;
const hours = Math.floor(totalSeconds / 3600);
const mins = Math.floor((totalSeconds % 3600) / 60);
const secs = totalSeconds % 60;
return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')},000`;
};
const downloadFile = (content: string, filename: string) => {
const element = document.createElement('a');
const file = new Blob([content], { type: 'text/plain' });
element.href = URL.createObjectURL(file);
element.download = filename;
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
URL.revokeObjectURL(element.href);
};
return {
downloadFullTranscript,
downloadTimestampedTranscript,
downloadSrtSubtitles
};
};How It Works
downloadFullTranscript()
Creates plain text file with continuous transcript:
- Maps over
transcriptDataextracting onlytextproperty - Joins all text with spaces (single paragraph format)
- Generates filename:
transcript-{video-title}.txt - Calls
downloadFile()to trigger browser download
Example Output:
Welcome to this video Today we'll be discussing React hooks Let's start with useState...downloadTimestampedTranscript()
Creates formatted text file with timestamps:
- Maps over
transcriptDataformatting each entry as:[startTime - endTime] text - Joins entries with newlines for separation
- Generates filename:
timestamped-transcript-{video-title}.txt - Calls
downloadFile()to trigger download
Example Output:
[0:00 - 0:03]
Welcome to this video
[0:03 - 0:07]
Today we'll be discussing React hooks
[0:07 - 0:12]
Let's start with useStatedownloadSrtSubtitles()
Creates SRT subtitle file:
- Maps over
transcriptDatawith index - Converts timestamps from "MM:SS" to SRT format "HH:MM:SS,000"
- Formats each entry as SRT subtitle block:
1 00:00:00,000 --> 00:00:03,000 Welcome to this video - Joins all blocks with newlines
- Generates filename:
subtitles-{video-title}.srt - Calls
downloadFile()to trigger download
convertToSrtTime(timeStr)
Converts "MM:SS" format to SRT time format "HH:MM:SS,000":
- Splits input by ":" and converts to numbers
- Calculates total seconds:
minutes * 60 + seconds - Extracts hours:
totalSeconds / 3600(floor) - Extracts minutes:
(totalSeconds % 3600) / 60(floor) - Extracts seconds:
totalSeconds % 60 - Pads each value to 2 digits with leading zeros
- Returns formatted string with ",000" milliseconds suffix
Example: "5:30" → "00:05:30,000"
downloadFile(content, filename)
Triggers browser download:
- Creates hidden anchor element (
<a>) - Creates Blob with content as plain text
- Generates object URL from Blob
- Sets
hrefto object URL - Sets
downloadattribute to filename - Appends element to document body
- Programmatically clicks element to trigger download
- Removes element from DOM
- Revokes object URL to free memory
This approach works across all modern browsers and doesn't require user interaction beyond clicking the download button.