Skip to main content
Glama
efikuta
by efikuta

generate_video_chapters

Create structured video chapters with timestamps and descriptions for YouTube videos using AI analysis of content.

Instructions

Generate AI-powered video chapters with timestamps and descriptions

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
videoIdYesYouTube video ID to generate chapters for
maxChaptersNoMaximum number of chapters to generate
minChapterLengthNoMinimum chapter length in seconds
includeDescriptionsNoInclude detailed chapter descriptions

Implementation Reference

  • ChapterGenerator class implementing the core logic for 'generate_video_chapters' tool. The execute() method orchestrates transcript fetching, AI-powered chapter detection using LLM, chapter enhancement, optimization, caching, and returns structured ChapterAnalysis.
    export class ChapterGenerator { constructor( private youtubeClient: YouTubeClient, private cache: CacheManager, private llmService: RobustLLMService, private transcriptProcessor: TranscriptProcessor, private logger: Logger ) {} async execute(args: unknown): Promise<ChapterAnalysis> { const params = GenerateVideoChaptersSchema.parse(args); this.logger.info(`Generating chapters for video ${params.videoId}`); // Generate cache key const cacheKey = `chapters:${params.videoId}:${params.minChapterLength}:${params.maxChapters}`; // Check cache first const cached = await this.cache.get<ChapterAnalysis>(cacheKey); if (cached) { this.logger.info(`Returning cached chapters for: ${params.videoId}`); return cached; } try { // Step 1: Get video details with transcript const videoDetails = await this.youtubeClient.getVideoDetails({ videoId: params.videoId, includeTranscript: true, includeComments: false }); if (!videoDetails.transcript || videoDetails.transcript.length === 0) { throw new Error(`No transcript available for video ${params.videoId}`); } // Step 2: Process transcript for structure analysis const processedTranscript = this.transcriptProcessor.processTranscript(videoDetails.transcript); // Step 3: Identify chapter boundaries using AI const chapters = await this.generateChapters( videoDetails.transcript, processedTranscript, videoDetails.video, params ); // Step 4: Enhance chapters with additional analysis const enhancedChapters = await this.enhanceChapters(chapters, videoDetails.transcript); // Step 5: Generate navigation and metadata const analysis = this.generateChapterAnalysis( params.videoId, enhancedChapters, videoDetails.video ); // Cache the result await this.cache.set(cacheKey, analysis, 3600); // 1 hour cache this.logger.info(`Chapter generation completed for ${params.videoId}: ${chapters.length} chapters created`); return analysis; } catch (error) { this.logger.error(`Failed to generate chapters for ${params.videoId}:`, error); throw error; } } /** * Generate chapters using AI analysis of transcript */ private async generateChapters( transcript: YouTubeTranscript[], processedTranscript: any, videoMetadata: any, params: GenerateVideoChaptersParams ): Promise<VideoChapter[]> { // For very long videos, analyze in segments const maxAnalysisLength = 15000; // ~15k characters if (processedTranscript.fullText.length > maxAnalysisLength) { return this.generateChaptersForLongVideo(transcript, videoMetadata, params); } else { return this.generateChaptersWithLLM(transcript, processedTranscript, videoMetadata, params); } } /** * Generate chapters using LLM for shorter videos */ private async generateChaptersWithLLM( transcript: YouTubeTranscript[], processedTranscript: any, videoMetadata: any, params: GenerateVideoChaptersParams ): Promise<VideoChapter[]> { const prompt = this.buildChapterGenerationPrompt( processedTranscript.fullText, videoMetadata, params ); try { const response = await this.llmService.generateWithFallback({ prompt, model: 'gpt-4o', // Use better model for complex structural analysis maxTokens: 1500, temperature: 0.1, responseFormat: 'json' }); const chapterData = JSON.parse(response.content); return this.processLLMChapterResponse(chapterData, transcript, params); } catch (error) { this.logger.warn('LLM chapter generation failed, using rule-based approach:', error); return this.generateChaptersRuleBased(transcript, params); } } /** * Generate chapters for long videos by analyzing segments */ private async generateChaptersForLongVideo( transcript: YouTubeTranscript[], videoMetadata: any, params: GenerateVideoChaptersParams ): Promise<VideoChapter[]> { const segmentDuration = 600; // 10-minute segments const segments = this.segmentTranscript(transcript, segmentDuration); const allChapters: VideoChapter[] = []; for (let i = 0; i < segments.length; i++) { const segment = segments[i]; try { const segmentText = segment.map(t => t.text).join(' '); const prompt = this.buildSegmentAnalysisPrompt(segmentText, i + 1, segments.length); const response = await this.llmService.generateWithFallback({ prompt, model: 'gpt-4o-mini', maxTokens: 800, temperature: 0.1, responseFormat: 'json' }); const segmentChapters = JSON.parse(response.content); if (segmentChapters.chapters && Array.isArray(segmentChapters.chapters)) { const processedChapters = segmentChapters.chapters.map((chapter: any) => ({ title: chapter.title, startTime: segment[0].start + (chapter.relativeStart || 0), endTime: segment[0].start + (chapter.relativeEnd || segment[segment.length - 1].start), duration: chapter.relativeEnd - chapter.relativeStart || 60, description: chapter.description, keyPoints: chapter.keyPoints || [], difficulty: chapter.difficulty || 'medium', importance: chapter.importance || 0.5 })); allChapters.push(...processedChapters); } // Small delay between segments if (i < segments.length - 1) { await new Promise(resolve => setTimeout(resolve, 200)); } } catch (error) { this.logger.warn(`Failed to analyze segment ${i + 1}:`, error); // Add basic chapter for this segment allChapters.push({ title: `Part ${i + 1}`, startTime: segment[0].start, endTime: segment[segment.length - 1].start + segment[segment.length - 1].duration, duration: segmentDuration, description: `Content from part ${i + 1} of the video`, keyPoints: [], difficulty: 'medium', importance: 0.5 }); } } // Merge and optimize chapters return this.optimizeChapters(allChapters, params); } /** * Build chapter generation prompt for LLM */ private buildChapterGenerationPrompt( transcriptText: string, videoMetadata: any, params: GenerateVideoChaptersParams ): string { return `You are an expert video editor who creates logical chapter breakdowns for educational content. Video Information: - Title: ${videoMetadata.title} - Duration: ${videoMetadata.duration} - Category: ${videoMetadata.categoryId} Requirements: - Create ${params.maxChapters} or fewer chapters - Each chapter should be at least ${params.minChapterLength} seconds - ${params.includeDescriptions ? 'Include detailed descriptions' : 'Brief descriptions only'} Transcript: ${transcriptText} Analyze the content flow and create logical chapter divisions. Look for: 1. Topic transitions 2. Natural pauses or transitions 3. Introduction/conclusion sections 4. Distinct concepts or lessons 5. Changes in speaking pace or tone Respond with JSON: { "chapters": [ { "title": "Clear, descriptive chapter title", "startTime": 0, "endTime": 120, "description": "What this chapter covers", "keyPoints": ["key point 1", "key point 2"], "difficulty": "easy|medium|hard", "importance": 0.8 } ] } Important: Base timing on content analysis, not arbitrary divisions.`; } /** * Build segment analysis prompt for long videos */ private buildSegmentAnalysisPrompt( segmentText: string, segmentNumber: number, totalSegments: number ): string { return `Analyze this segment (${segmentNumber}/${totalSegments}) of a video transcript and identify 1-3 logical chapters within it. Segment Content: ${segmentText} Focus on natural topic transitions and content changes. Each chapter should represent a distinct concept or discussion point. Respond with JSON: { "chapters": [ { "title": "Chapter title", "relativeStart": 0, "relativeEnd": 180, "description": "Brief description", "keyPoints": ["point 1", "point 2"], "difficulty": "medium", "importance": 0.7 } ] } Note: Use relative timestamps within this segment (0 = segment start).`; } /** * Process LLM chapter response into VideoChapter objects */ private processLLMChapterResponse( chapterData: any, transcript: YouTubeTranscript[], params: GenerateVideoChaptersParams ): VideoChapter[] { if (!chapterData.chapters || !Array.isArray(chapterData.chapters)) { throw new Error('Invalid chapter response format'); } const videoDuration = transcript[transcript.length - 1]?.start + transcript[transcript.length - 1]?.duration || 0; return chapterData.chapters .filter((chapter: any) => chapter.endTime - chapter.startTime >= params.minChapterLength ) .map((chapter: any) => ({ title: chapter.title || 'Untitled Chapter', startTime: Math.max(0, chapter.startTime || 0), endTime: Math.min(videoDuration, chapter.endTime || chapter.startTime + params.minChapterLength), duration: (chapter.endTime || 0) - (chapter.startTime || 0), description: chapter.description || '', keyPoints: Array.isArray(chapter.keyPoints) ? chapter.keyPoints : [], difficulty: ['easy', 'medium', 'hard'].includes(chapter.difficulty) ? chapter.difficulty : 'medium', importance: Math.max(0, Math.min(1, chapter.importance || 0.5)) })) .slice(0, params.maxChapters); } /** * Rule-based chapter generation fallback */ private generateChaptersRuleBased( transcript: YouTubeTranscript[], params: GenerateVideoChaptersParams ): VideoChapter[] { const videoDuration = transcript[transcript.length - 1]?.start + transcript[transcript.length - 1]?.duration || 0; const idealChapterLength = Math.max(params.minChapterLength, videoDuration / params.maxChapters); const chapters: VideoChapter[] = []; let currentStart = 0; let chapterIndex = 1; while (currentStart < videoDuration && chapters.length < params.maxChapters) { const endTime = Math.min(currentStart + idealChapterLength, videoDuration); // Find natural break points (silence or topic changes) const adjustedEndTime = this.findNaturalBreakPoint(transcript, currentStart, endTime); // Get content for this chapter const chapterTranscript = transcript.filter(t => t.start >= currentStart && t.start < adjustedEndTime ); const chapterText = chapterTranscript.map(t => t.text).join(' '); const keyPoints = this.extractKeyPointsRuleBased(chapterText); chapters.push({ title: `Chapter ${chapterIndex}`, startTime: Math.round(currentStart), endTime: Math.round(adjustedEndTime), duration: Math.round(adjustedEndTime - currentStart), description: `Content from ${this.formatTime(currentStart)} to ${this.formatTime(adjustedEndTime)}`, keyPoints, difficulty: 'medium', importance: 0.6 }); currentStart = adjustedEndTime; chapterIndex++; } return chapters; } /** * Find natural break points in transcript */ private findNaturalBreakPoint( transcript: YouTubeTranscript[], startTime: number, idealEndTime: number ): number { const searchWindow = 30; // Look within 30 seconds of ideal end time const candidates = transcript.filter(t => t.start >= idealEndTime - searchWindow && t.start <= idealEndTime + searchWindow ); // Look for natural pause indicators const breakIndicators = ['.', '!', '?', 'so', 'now', 'next', 'alright', 'okay']; for (const candidate of candidates) { const text = candidate.text.toLowerCase(); if (breakIndicators.some(indicator => text.includes(indicator))) { return candidate.start; } } return idealEndTime; // Fallback to ideal time } /** * Extract key points using rule-based approach */ private extractKeyPointsRuleBased(text: string): string[] { const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 20); // Look for sentences with important indicators const importantSentences = sentences.filter(sentence => { const lower = sentence.toLowerCase(); return ( lower.includes('important') || lower.includes('key') || lower.includes('remember') || lower.includes('main') || lower.includes('first') || lower.includes('second') || lower.includes('finally') ); }); return importantSentences.slice(0, 3).map(s => s.trim()); } /** * Enhance chapters with additional analysis */ private async enhanceChapters( chapters: VideoChapter[], transcript: YouTubeTranscript[] ): Promise<VideoChapter[]> { return Promise.all( chapters.map(async (chapter) => { try { // Get transcript for this chapter const chapterTranscript = transcript.filter(t => t.start >= chapter.startTime && t.start < chapter.endTime ); // Enhance with better descriptions if possible if (chapterTranscript.length > 0) { const enhanced = await this.enhanceChapterDescription(chapter, chapterTranscript); return enhanced; } return chapter; } catch (error) { this.logger.warn(`Failed to enhance chapter "${chapter.title}":`, error); return chapter; } }) ); } /** * Enhance individual chapter description using LLM */ private async enhanceChapterDescription( chapter: VideoChapter, chapterTranscript: YouTubeTranscript[] ): Promise<VideoChapter> { try { const chapterText = chapterTranscript.map(t => t.text).join(' '); if (chapterText.length < 50) return chapter; // Skip very short chapters const prompt = `Analyze this chapter content and create a better title and description. Chapter Title: ${chapter.title} Duration: ${this.formatTime(chapter.duration)} Content: ${chapterText.slice(0, 1000)}... Create: 1. A more descriptive title (max 6 words) 2. A brief description of what's covered 3. 2-3 key takeaways Respond with JSON: { "title": "Better chapter title", "description": "What this chapter covers", "keyPoints": ["takeaway 1", "takeaway 2"] }`; const response = await this.llmService.generateWithFallback({ prompt, model: 'gpt-4o-mini', maxTokens: 300, temperature: 0.2, responseFormat: 'json' }); const enhancement = JSON.parse(response.content); return { ...chapter, title: enhancement.title || chapter.title, description: enhancement.description || chapter.description, keyPoints: enhancement.keyPoints || chapter.keyPoints }; } catch (error) { this.logger.warn(`Failed to enhance chapter description:`, error); return chapter; } } /** * Generate comprehensive chapter analysis */ private generateChapterAnalysis( videoId: string, chapters: VideoChapter[], videoMetadata: any ): ChapterAnalysis { const avgChapterLength = chapters.length > 0 ? chapters.reduce((sum, ch) => sum + ch.duration, 0) / chapters.length : 0; // Analyze content flow const contentFlow = this.analyzeContentFlow(chapters); // Analyze difficulty progression const difficultyProgression = chapters.map(ch => ch.difficulty); // Find recommended break points (chapters with high importance) const recommendedBreakpoints = chapters .filter(ch => ch.importance > 0.7) .map(ch => ch.startTime); // Create quick access navigation const quickAccess = this.createQuickAccessNavigation(chapters); // Generate learning path suggestions const learningPath = this.generateLearningPath(chapters); // Identify skippable sections const skipSuggestions = chapters .filter(ch => ch.difficulty === 'easy' && ch.importance < 0.4) .map(ch => chapters.indexOf(ch)); return { videoId, chapters, metadata: { totalChapters: chapters.length, avgChapterLength: Math.round(avgChapterLength), contentFlow, difficultyProgression, recommendedBreakpoints }, navigation: { quickAccess, learningPath, skipSuggestions } }; } // Helper methods private segmentTranscript(transcript: YouTubeTranscript[], segmentDuration: number): YouTubeTranscript[][] { const segments: YouTubeTranscript[][] = []; let currentSegment: YouTubeTranscript[] = []; let segmentStartTime = 0; for (const item of transcript) { if (item.start - segmentStartTime >= segmentDuration && currentSegment.length > 0) { segments.push(currentSegment); currentSegment = [item]; segmentStartTime = item.start; } else { currentSegment.push(item); } } if (currentSegment.length > 0) { segments.push(currentSegment); } return segments; } private optimizeChapters(chapters: VideoChapter[], params: GenerateVideoChaptersParams): VideoChapter[] { // Remove very short chapters and merge if necessary let optimized = chapters.filter(ch => ch.duration >= params.minChapterLength); // If too many chapters, merge similar adjacent ones if (optimized.length > params.maxChapters) { optimized = this.mergeChapters(optimized, params.maxChapters); } return optimized; } private mergeChapters(chapters: VideoChapter[], maxChapters: number): VideoChapter[] { if (chapters.length <= maxChapters) return chapters; // Simple merging strategy: combine adjacent chapters with similar difficulty const merged: VideoChapter[] = []; for (let i = 0; i < chapters.length; i++) { if (merged.length >= maxChapters) break; const current = chapters[i]; const next = chapters[i + 1]; if (next && merged.length < maxChapters - 1 && current.difficulty === next.difficulty) { // Merge current and next merged.push({ title: `${current.title} & ${next.title}`, startTime: current.startTime, endTime: next.endTime, duration: next.endTime - current.startTime, description: `${current.description} ${next.description}`, keyPoints: [...current.keyPoints, ...next.keyPoints], difficulty: current.difficulty, importance: Math.max(current.importance, next.importance) }); i++; // Skip next chapter as it's been merged } else { merged.push(current); } } return merged; } private analyzeContentFlow(chapters: VideoChapter[]): string { const difficulties = chapters.map(ch => ch.difficulty); if (difficulties.every(d => d === 'easy')) return 'Consistently easy throughout'; if (difficulties.every(d => d === 'hard')) return 'Consistently challenging'; // Check for logical progression const progression = difficulties.join(' -> '); if (progression.includes('easy') && progression.includes('hard')) { return 'Progresses from easy to complex topics'; } return 'Mixed difficulty levels'; } private createQuickAccessNavigation(chapters: VideoChapter[]): Array<{ topic: string; chapters: number[] }> { // Group chapters by common keywords in titles const topicGroups: Record<string, number[]> = {}; chapters.forEach((chapter, index) => { const words = chapter.title.toLowerCase().split(' '); const significantWords = words.filter(word => word.length > 3 && !['the', 'and', 'for', 'with'].includes(word) ); significantWords.forEach(word => { if (!topicGroups[word]) topicGroups[word] = []; topicGroups[word].push(index); }); }); return Object.entries(topicGroups) .filter(([_, chapters]) => chapters.length > 1) .slice(0, 5) // Top 5 topics .map(([topic, chapterIndices]) => ({ topic, chapters: chapterIndices })); } private generateLearningPath(chapters: VideoChapter[]): string[] { return chapters .sort((a, b) => { // Sort by difficulty, then by importance const diffOrder = { easy: 1, medium: 2, hard: 3 }; const aDiff = diffOrder[a.difficulty] || 2; const bDiff = diffOrder[b.difficulty] || 2; if (aDiff !== bDiff) return aDiff - bDiff; return b.importance - a.importance; }) .map(ch => ch.title); } private formatTime(seconds: number): string { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = Math.floor(seconds % 60); if (hours > 0) { return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; } return `${minutes}:${secs.toString().padStart(2, '0')}`; } }
  • Zod schema defining input parameters and validation for generate_video_chapters tool, used in handler for parsing args.
    export const GenerateVideoChaptersSchema = z.object({ videoId: z.string().describe('YouTube video ID'), minChapterLength: z.number().min(30).max(600).default(120).describe('Minimum chapter length in seconds'), maxChapters: z.number().min(3).max(20).default(10).describe('Maximum number of chapters'), includeDescriptions: z.boolean().default(true).describe('Include chapter descriptions'), });
  • src/index.ts:592-594 (registration)
    Tool call routing in MCP CallToolRequestSchema handler - dispatches generate_video_chapters calls to ChapterGenerator.execute()
    case 'generate_video_chapters': result = await this.chapterTool.execute(args); break;
  • src/index.ts:475-506 (registration)
    Tool registration metadata in ListToolsRequestSchema response - defines name, description, and input schema for client discovery.
    name: 'generate_video_chapters', description: 'Generate AI-powered video chapters with timestamps and descriptions', inputSchema: { type: 'object', properties: { videoId: { type: 'string', description: 'YouTube video ID to generate chapters for' }, maxChapters: { type: 'number', minimum: 3, maximum: 20, default: 10, description: 'Maximum number of chapters to generate' }, minChapterLength: { type: 'number', minimum: 30, maximum: 600, default: 60, description: 'Minimum chapter length in seconds' }, includeDescriptions: { type: 'boolean', default: true, description: 'Include detailed chapter descriptions' } }, required: ['videoId'] } },
  • src/index.ts:181-182 (registration)
    Instantiation of ChapterGenerator handler instance with required dependencies (YouTubeClient, CacheManager, LLMService, etc.) in main MCP server class.
    this.chapterTool = new ChapterGenerator(this.youtubeClient, this.cache, this.llmService, this.transcriptProcessor, this.logger); this.knowledgeGraphTool = new KnowledgeGraphGenerator(this.youtubeClient, this.cache, this.llmService, this.transcriptProcessor, this.logger);

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/efikuta/youtube-knowledge-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server