{ "name": "YouTube Transcript → Executive Summary (Free)", "nodes": [ { "parameters": { "httpMethod": "POST", "path": "yt-transcript-lite", "responseMode": "responseNode", "options": {} }, "id": "webhook", "name": "Webhook", "type": "n8n-nodes-base.webhook", "typeVersion": 2, "position": [ 250, 300 ], "webhookId": "yt-transcript-lite" }, { "parameters": { "jsCode": "const url = $input.first().json.body.url;\n// Extract video ID from various YouTube URL formats\nlet videoId = null;\nconst patterns = [\n /(?:youtube\\.com\\/watch\\?v=|youtu\\.be\\/|youtube\\.com\\/embed\\/|youtube\\.com\\/v\\/)([a-zA-Z0-9_-]{11})/,\n /^([a-zA-Z0-9_-]{11})$/\n];\nfor (const pattern of patterns) {\n const match = url.match(pattern);\n if (match) {\n videoId = match[1];\n break;\n }\n}\nif (!videoId) {\n throw new Error('Invalid YouTube URL. Could not extract video ID.');\n}\nreturn { videoId, url };" }, "id": "extract-id", "name": "Extract Video ID", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 450, 300 ] }, { "parameters": { "jsCode": "// ============================================================\n// Fetch Transcript with automatic fallback (v9)\n// Primary: youtubetranscript.com (free, keyless — same as paid)\n// Fallback: local bridge using youtube-transcript-api (optional)\n// ============================================================\nconst videoId = $json.videoId;\n// Optional secondary source. Leave empty to disable.\n// Point at any transcript API you have (e.g. a local bridge or\n// transcriptapi.com with your key). Primary source is free & keyless.\nconst FALLBACK_URL = '';\n\nlet raw = null;\nlet source = 'youtubetranscript.com';\n\n// 1) Primary source (free, no API key)\ntry {\n const resp = await this.helpers.httpRequest({\n method: 'GET',\n url: `https://youtubetranscript.com/?server_vid2=${videoId}`,\n responseFormat: 'text',\n timeout: 20000,\n });\n const isBlocked = /currently blocking us|working on a fix|can't fetch|unable to fetch/i.test(resp) && resp.length < 400;\n if (!isBlocked && resp.includes(' try fallback\n}\n\n// 2) Fallback source (local bridge / any configured transcript API)\nif (!raw && FALLBACK_URL) {\n try {\n const fb = await this.helpers.httpRequest({\n method: 'GET',\n url: `${FALLBACK_URL}?video_id=${videoId}`,\n json: true,\n timeout: 30000,\n });\n if (fb && fb.ok && fb.transcript) {\n raw = fb.transcript;\n source = 'local-bridge';\n }\n } catch (e) {\n // fallback unavailable\n }\n}\n\nif (!raw) {\n throw new Error('Could not fetch a transcript for this video. The free transcript service is temporarily blocked by YouTube — try a video with captions, or try again later.');\n}\n\n// Parse: XML tags (primary) or plain text (fallback)\nlet transcript = '';\nconst textMatches = raw.match(/]*>([\\s\\S]*?)<\\/text>/g) || [];\nif (textMatches.length) {\n transcript = textMatches\n .map(t => t.replace(/<[^>]+>/g, '').trim())\n .filter(t => t.length > 0)\n .join(' ');\n} else {\n transcript = raw\n .split('\\n')\n .filter(l => l.trim() && !l.includes('[') && !l.includes('((') && l.length > 5)\n .join(' ');\n}\ntranscript = transcript.trim();\nif (!transcript) {\n throw new Error('Could not extract a transcript for this video. It may have no captions, or the transcript service is unavailable.');\n}\n\nreturn {\n transcript: transcript.substring(0, 25000),\n wordCount: transcript.split(' ').length,\n source,\n};" }, "id": "fetch-transcript", "name": "Fetch Transcript", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 650, 300 ] }, { "parameters": { "resource": "chat", "operation": "complete", "chatModel": "gpt-4o-mini", "messages": { "messages": [ { "role": "system", "content": "You are a YouTube content analyst. Analyze the provided transcript and produce an **Executive Summary** (3-5 paragraphs) that captures the key points, insights, and main message of the video. Return as structured JSON with key: summary" }, { "role": "user", "content": "={{ $json.transcript }}" } ] }, "simplifyOutput": true, "options": { "temperature": 0.3, "maxTokens": 1000 } }, "id": "ai-analysis", "name": "AI Analysis", "type": "n8n-nodes-base.openAi", "typeVersion": 2, "position": [ 850, 300 ] }, { "parameters": { "jsCode": "// OpenAI node (simplifyOutput: true) returns { message: { role, content } }\n// The model is instructed to return JSON with key \"summary\".\nconst msg = $input.first().json.message;\nconst content = (msg && msg.content) || '';\nlet summary = content.trim();\ntry {\n const parsed = JSON.parse(content);\n if (parsed && parsed.summary) {\n summary = parsed.summary;\n }\n} catch (e) {\n // content was plain text\n}\n\nconst prev = $('Fetch Transcript').first().json;\nreturn {\n videoUrl: $('Extract Video ID').first().json.url || '',\n videoId: $('Extract Video ID').first().json.videoId || '',\n wordCount: prev.wordCount || 0,\n summary,\n};" }, "id": "format-output", "name": "Format Output", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1050, 300 ] }, { "parameters": { "options": { "response": { "responseCode": 200, "respondMode": "allJson", "responseHeaders": { "entries": [ { "name": "Content-Type", "value": "application/json" } ] } } }, "jsCode": "const output = $input.first().json;\nreturn output;" }, "id": "respond", "name": "Respond to Webhook", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1, "position": [ 1250, 300 ] } ], "connections": { "Webhook": { "main": [ [ { "node": "Extract Video ID", "type": "main", "index": 0 } ] ] }, "Extract Video ID": { "main": [ [ { "node": "Fetch Transcript", "type": "main", "index": 0 } ] ] }, "Fetch Transcript": { "main": [ [ { "node": "AI Analysis", "type": "main", "index": 0 } ] ] }, "AI Analysis": { "main": [ [ { "node": "Format Output", "type": "main", "index": 0 } ] ] }, "Format Output": { "main": [ [ { "node": "Respond to Webhook", "type": "main", "index": 0 } ] ] } }, "settings": { "executionOrder": "v1" }, "staticData": null, "tags": [ "youtube", "transcript", "summary", "openai", "content-creation" ] }