Konvertieren Sie PDF-Artikel in Podcasts - N8N
Dies ist ein Automatisierungsworkflow mit 17 Nodes. Hauptsächlich werden Set, Code, Merge, ReadPDF, EmailSend und andere Nodes verwendet. Konvertieren Sie PDF-Artikel mit Google TTS und Cloudflare R2 in Audio-Podcasts
- •Möglicherweise sind Ziel-API-Anmeldedaten erforderlich
Verwendete Nodes (17)
Kategorie
{
"name": "Convert PDF Articles to Podcast - N8N",
"nodes": [
{
"id": "8616c449-1148-438e-b1a0-57dcc9b6caed",
"name": "⚙️ Workflow-Konfiguration",
"type": "n8n-nodes-base.set",
"position": [
32,
480
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "r2-public-url",
"name": "R2_PUBLIC_URL",
"type": "string",
"value": "https://pub-YOUR-R2-ID-HERE.r2.dev"
},
{
"id": "r2-bucket",
"name": "R2_BUCKET",
"type": "string",
"value": "your-bucket-name"
},
{
"id": "rss-filename",
"name": "RSS_FILENAME",
"type": "string",
"value": "your-podcast-feed.xml"
},
{
"id": "c7427d04-8c09-4e62-bc17-073033449664",
"name": "PODCAST_ARTWORK_URL",
"type": "string",
"value": "https://pub-YOUR-R2-ID-HERE.r2.dev/artwork/podcast-image.png"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "0cb6f54b-34b7-449c-b42c-1a8eb341001a",
"name": "PDF für Podcast hochladen",
"type": "n8n-nodes-base.formTrigger",
"position": [
80,
128
],
"parameters": {
"options": {},
"formTitle": "Upload PDF for Podcast Conversion",
"formFields": {
"values": [
{
"fieldType": "file",
"fieldLabel": "pdfFile",
"multipleFiles": false,
"requiredField": true,
"acceptFileTypes": "pdf"
}
]
},
"formDescription": "Upload a PDF file to convert to audio podcast. Processing will start immediately after upload"
},
"typeVersion": 2.3
},
{
"id": "4f7236ac-852d-4c6f-9393-1fd6ed1c5610",
"name": "PDF-Text extrahieren",
"type": "n8n-nodes-base.readPDF",
"position": [
336,
128
],
"parameters": {
"binaryPropertyName": "pdfFile"
},
"typeVersion": 1
},
{
"id": "b533785e-e3bb-4bc6-b3a4-ab1f452ae330",
"name": "Text bereinigen & verarbeiten",
"type": "n8n-nodes-base.code",
"position": [
544,
128
],
"parameters": {
"jsCode": "// Get PDF text and metadata\nconst pdfText = $input.item.json.text;\nconst fileName = $input.item.binary.pdfFile?.fileName || 'uploaded-document.pdf';\nconst documentTitle = fileName.replace('.pdf', '').replace(/_/g, ' ');\n\n// Clean text\nlet cleanedText = pdfText\n .replace(/\\f/g, '\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .replace(/^\\d+\\s*$/gm, '')\n .replace(/PSYCHOLOGY SECONDARY COURSE/gi, '')\n .replace(/MODULE - I.*?Psychology/gis, '')\n .trim();\n\n// Extract document metadata\nconst wordCount = cleanedText.split(/\\s+/).length;\nconst charCount = cleanedText.length;\nconst estimatedMinutes = Math.ceil(charCount / 1000);\n\nreturn {\n json: {\n documentTitle,\n fileName,\n cleanedText,\n wordCount,\n charCount,\n estimatedMinutes,\n uploadDate: new Date().toISOString()\n }\n};"
},
"typeVersion": 2
},
{
"id": "aac02256-376d-421d-897b-06bdfd1b9494",
"name": "Abschnitte erkennen & aufteilen",
"type": "n8n-nodes-base.code",
"position": [
736,
128
],
"parameters": {
"jsCode": "const input = $input.item.json;\nconst cleanedText = input.cleanedText;\nconst documentTitle = input.documentTitle;\n\nconst MAX_BYTES = 4500;\n\nconst sectionRegex = /(\\d+\\.\\d+)\\s+([A-Z][A-Z\\s]{10,})/g;\nlet sections = [];\nlet lastIndex = 0;\nlet match;\n\nwhile ((match = sectionRegex.exec(cleanedText)) !== null) {\n const sectionNumber = match[1];\n const sectionTitle = match[2].trim();\n const startIndex = match.index;\n\n if (lastIndex < startIndex) {\n const prevText = cleanedText.substring(lastIndex, startIndex).trim();\n if (prevText.length > 100) {\n sections.push({ text: prevText, number: sections.length + 1, title: 'Introduction' });\n }\n }\n\n sections.push({ number: sectionNumber, title: sectionTitle, startIndex });\n lastIndex = startIndex;\n}\n\nconst episodes = [];\nfor (let i = 0; i < sections.length; i++) {\n const section = sections[i];\n const nextSection = sections[i + 1];\n const endIndex = nextSection ? nextSection.startIndex : cleanedText.length;\n const sectionText = cleanedText.substring(section.startIndex, endIndex).trim();\n\n if (Buffer.byteLength(sectionText, 'utf8') > MAX_BYTES) {\n const chunks = splitTextIntoChunks(sectionText, MAX_BYTES);\n chunks.forEach((chunk, idx) => {\n episodes.push({\n episodeNumber: `${section.number}.${idx + 1}`,\n episodeTitle: `${documentTitle} - Section ${section.number}.${idx + 1}: ${section.title}`,\n sectionTitle: section.title,\n textContent: chunk,\n charCount: chunk.length,\n totalEpisodes: null\n });\n });\n } else {\n episodes.push({\n episodeNumber: section.number,\n episodeTitle: `${documentTitle} - Section ${section.number}: ${section.title}`,\n sectionTitle: section.title,\n textContent: sectionText,\n charCount: sectionText.length,\n totalEpisodes: null\n });\n }\n}\n\nif (episodes.length === 0) {\n const chunks = splitTextIntoChunks(cleanedText, MAX_BYTES);\n chunks.forEach((chunk, idx) => {\n episodes.push({\n episodeNumber: idx + 1,\n episodeTitle: `${documentTitle} - Part ${idx + 1}`,\n sectionTitle: `Part ${idx + 1}`,\n textContent: chunk,\n charCount: chunk.length,\n totalEpisodes: chunks.length\n });\n });\n}\n\nepisodes.forEach(ep => ep.totalEpisodes = episodes.length);\n\nfunction splitTextIntoChunks(text, maxBytes) {\n const chunks = [];\n let currentChunk = '';\n const sentences = text.split(/(?<=[.!?])\\s+/);\n\n for (const sentence of sentences) {\n const testChunk = currentChunk + (currentChunk ? ' ' : '') + sentence;\n if (Buffer.byteLength(testChunk, 'utf8') > maxBytes) {\n if (currentChunk) {\n chunks.push(currentChunk);\n currentChunk = sentence;\n } else {\n const words = sentence.split(' ');\n for (const word of words) {\n const testWord = currentChunk + (currentChunk ? ' ' : '') + word;\n if (Buffer.byteLength(testWord, 'utf8') > maxBytes) {\n chunks.push(currentChunk);\n currentChunk = word;\n } else {\n currentChunk = testWord;\n }\n }\n }\n } else {\n currentChunk = testChunk;\n }\n }\n\n if (currentChunk) {\n chunks.push(currentChunk);\n }\n\n return chunks;\n}\n\nreturn episodes.map(ep => ({ json: ep }));"
},
"typeVersion": 2
},
{
"id": "3a32b49b-bcac-4a61-b405-2f5d412a1191",
"name": "TTS-Nutzungslimit prüfen",
"type": "n8n-nodes-base.code",
"position": [
944,
128
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const now = new Date();\nconst monthKey = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;\n\nconst monthlyUsage = $getWorkflowStaticData('global');\n\nif (!monthlyUsage[monthKey]) {\n monthlyUsage[monthKey] = { charCount: 0, requestCount: 0 };\n}\n\nconst currentUsage = monthlyUsage[monthKey].charCount;\nconst charCount = $input.item.json.charCount;\nconst MONTHLY_LIMIT = 950000;\n\nif (currentUsage + charCount > MONTHLY_LIMIT) {\n throw new Error(\n `⚠️ MONTHLY TTS LIMIT REACHED!\\n` +\n `Current: ${currentUsage.toLocaleString()} chars\\n` +\n `This request: ${charCount.toLocaleString()} chars\\n` +\n `Limit: ${MONTHLY_LIMIT.toLocaleString()} chars`\n );\n}\n\nreturn {\n json: {\n ...$input.item.json,\n usageInfo: {\n currentMonthUsage: currentUsage,\n thisRequestChars: charCount,\n remainingChars: MONTHLY_LIMIT - currentUsage - charCount,\n monthKey\n }\n }\n};"
},
"typeVersion": 2
},
{
"id": "ce537777-d89c-445b-b1ed-199beac096ce",
"name": "Google TTS API",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"position": [
1136,
128
],
"parameters": {
"url": "https://texttospeech.googleapis.com/v1/text:synthesize",
"method": "POST",
"options": {
"timeout": 60000,
"response": {
"response": {
"responseFormat": "json"
}
}
},
"sendBody": true,
"authentication": "genericCredentialType",
"bodyParameters": {
"parameters": [
{
"name": "input.text",
"value": "={{ $json.textContent }}"
},
{
"name": "voice.languageCode",
"value": "en-US"
},
{
"name": "voice.name",
"value": "en-US-Wavenet-D"
},
{
"name": "audioConfig.audioEncoding",
"value": "MP3"
},
{
"name": "audioConfig.speakingRate",
"value": "1.0"
},
{
"name": "audioConfig.pitch",
"value": "0.0"
}
]
},
"genericAuthType": "httpHeaderAuth"
},
"retryOnFail": true,
"typeVersion": 4.2,
"alwaysOutputData": true,
"waitBetweenTries": 2000
},
{
"id": "7948efd4-3d73-4496-95fa-12d25ff97fe0",
"name": "Audio in Binärdaten umwandeln",
"type": "n8n-nodes-base.code",
"position": [
1344,
128
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const audioBase64 = $input.item.json.audioContent;\n\nif (!audioBase64) {\n console.log(`Skipping item ${$itemIndex} - no audio content`);\n return null;\n}\n\nconst allSectionItems = $('Detect Sections & Split').all();\nconst sectionData = allSectionItems[$itemIndex] ? allSectionItems[$itemIndex].json : {};\n\nconst episodeTitle = sectionData.episodeTitle || 'Episode';\nconst episodeNumber = sectionData.episodeNumber || 'N/A';\nconst sectionTitle = sectionData.sectionTitle || '';\nconst charCount = sectionData.charCount || 0;\n\nconst date = new Date().toISOString().split('T')[0];\nconst safeTitle = episodeTitle\n .replace(/[^a-zA-Z0-9\\s-]/g, '')\n .replace(/\\s+/g, '_')\n .substring(0, 100);\nconst fileName = `${safeTitle}_${date}.mp3`;\n\nconst binaryBuffer = Buffer.from(audioBase64, 'base64');\n\nreturn {\n json: {\n fileName,\n fileSize: binaryBuffer.length,\n mimeType: 'audio/mpeg',\n episodeTitle,\n episodeNumber,\n sectionTitle,\n charCount,\n audioContent: audioBase64\n },\n binary: {\n audioFile: {\n data: audioBase64,\n mimeType: 'audio/mpeg',\n fileName: fileName,\n fileExtension: 'mp3'\n }\n }\n};"
},
"typeVersion": 2
},
{
"id": "9b9c9e38-5b5a-491f-b3b7-5d0d39c73ad5",
"name": "Alle MP3s zusammenfügen",
"type": "n8n-nodes-base.code",
"position": [
32,
336
],
"parameters": {
"jsCode": "const allItems = $input.all();\n\nconst sortedItems = allItems.sort((a, b) => {\n const numA = a.json.episodeNumber || 0;\n const numB = b.json.episodeNumber || 0;\n return numA - numB;\n});\n\nconst mp3Buffers = [];\nlet totalSize = 0;\nlet totalChars = 0;\nconst episodesList = [];\n\nfor (const item of sortedItems) {\n const audioData = item.json.audioContent;\n const buffer = Buffer.from(audioData, 'base64');\n mp3Buffers.push(buffer);\n totalSize += buffer.length;\n totalChars += item.json.charCount || 0;\n\n episodesList.push({\n number: item.json.episodeNumber,\n title: item.json.episodeTitle,\n charCount: item.json.charCount\n });\n}\n\nconst combinedBuffer = Buffer.concat(mp3Buffers);\nconst combinedBase64 = combinedBuffer.toString('base64');\n\nconst date = new Date().toISOString().split('T')[0];\nconst fileName = `Chapter-2_Complete_${date}.mp3`;\n\nreturn [{\n json: {\n fileName,\n fileSize: totalSize,\n mimeType: 'audio/mpeg',\n totalEpisodes: sortedItems.length,\n totalCharacters: totalChars,\n episodes: episodesList,\n episodeTitle: `Chapter-2 - Complete (${sortedItems.length} sections)`,\n episodeNumber: 'Complete',\n charCount: totalChars\n },\n binary: {\n audioFile: {\n data: combinedBase64,\n mimeType: 'audio/mpeg',\n fileName: fileName,\n fileExtension: 'mp3'\n }\n }\n}];"
},
"typeVersion": 2
},
{
"id": "72ae9ece-7ea5-48f8-969c-79f70376ff1b",
"name": "MP3 in R2 hochladen",
"type": "n8n-nodes-cloudflare-r2-storage.cloudflareR2Storage",
"position": [
432,
384
],
"parameters": {
"objectKey": "={{ $json.fileName }}",
"bucketName": "={{ $('⚙️ Workflow Config').first().json.R2_BUCKET }}",
"contentType": "audio/mpeg",
"binaryPropertyName": "audioFile"
},
"typeVersion": 1
},
{
"id": "9789ab0b-b679-4314-a814-5407a42fb7d1",
"name": "RSS XML erstellen",
"type": "n8n-nodes-base.code",
"position": [
624,
384
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// Enhanced RSS Feed Code v2 - \"Build RSS XML\" Node\n// Copy this ENTIRE code and replace the existing code in \"Build RSS XML\" node\n\n// Get data from Merge node (has original MP3 data)\nconst mp3Data = $('Merge').first().json;\nconst episodes = mp3Data.episodes || [];\nconst totalEpisodes = mp3Data.totalEpisodes || 1;\nconst fileName = mp3Data.fileName || 'Podcast.mp3';\nconst fileSize = mp3Data.fileSize || 0;\n\n// Get configuration from standalone config node\nconst config = $('⚙️ Workflow Config').first().json;\nconst R2_PUBLIC_URL = config.R2_PUBLIC_URL;\nconst RSS_FILENAME = config.RSS_FILENAME;\nconst PODCAST_ARTWORK_URL = config.PODCAST_ARTWORK_URL || \"https://via.placeholder.com/3000x3000.png?text=Personal+Podcast\";\n\n// Construct R2 public URLs\nconst r2Mp3Url = `${R2_PUBLIC_URL}/${fileName}`;\nconst pubDate = new Date().toUTCString();\n\n// Extract document title\nconst docTitle = fileName.replace(/_Complete_\\d{4}-\\d{2}-\\d{2}\\.mp3$/, '').replace(/_/g, ' ');\n\n// Calculate estimated duration (MP3 128kbps = ~16KB/sec)\nconst estimatedDurationSeconds = Math.round(fileSize / 16000);\nconst durationHours = Math.floor(estimatedDurationSeconds / 3600);\nconst durationMinutes = Math.floor((estimatedDurationSeconds % 3600) / 60);\nconst durationSeconds = estimatedDurationSeconds % 60;\nconst durationFormatted = durationHours > 0\n ? `${durationHours}:${String(durationMinutes).padStart(2, '0')}:${String(durationSeconds).padStart(2, '0')}`\n : `${durationMinutes}:${String(durationSeconds).padStart(2, '0')}`;\n\n// Build episode description (plain text)\nconst episodeDescription = episodes.map((ep, idx) =>\n `${idx + 1}. ${ep.title} (${ep.charCount.toLocaleString()} characters)`\n).join('\\n');\n\n// Build episode description (HTML for better formatting)\nconst episodeDescriptionHTML = episodes.map((ep, idx) =>\n `<p><strong>${idx + 1}. ${ep.title}</strong><br/><em>${ep.charCount.toLocaleString()} characters</em></p>`\n).join('\\n');\n\n// Current year for copyright\nconst currentYear = new Date().getFullYear();\n\n// Build enhanced RSS feed XML with improved metadata\nconst rssXml = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<rss version=\"2.0\"\n xmlns:itunes=\"http://www.itunes.com/dtds/podcast-1.0.dtd\"\n xmlns:atom=\"http://www.w3.org/2005/Atom\"\n xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">\n <channel>\n <title>Personal Podcast</title>\n <description>Transform any PDF article, research paper, or document into high-quality audio. Perfect for learning while commuting, exercising, or multitasking.</description>\n <language>en-us</language>\n <link>https://your-website-url.com</link>\n <atom:link href=\"${R2_PUBLIC_URL}/${RSS_FILENAME}\" rel=\"self\" type=\"application/rss+xml\"/>\n\n <!-- iTunes Podcast Metadata -->\n <itunes:author>Your Name</itunes:author>\n <itunes:owner>\n <itunes:name>Your Name</itunes:name>\n <itunes:email>your-email@example.com</itunes:email>\n </itunes:owner>\n <itunes:image href=\"${PODCAST_ARTWORK_URL}\"/>\n <itunes:category text=\"Education\">\n <itunes:category text=\"Self-Improvement\"/>\n </itunes:category>\n <itunes:category text=\"Technology\"/>\n <itunes:explicit>no</itunes:explicit>\n <itunes:type>episodic</itunes:type>\n <itunes:summary>Your personal AI narrator for all your reading materials. Convert PDFs to audio instantly.</itunes:summary>\n <itunes:subtitle>PDF Articles converted to audio for personal listening</itunes:subtitle>\n\n <!-- Podcast Metadata -->\n <copyright>© ${currentYear} Your Podcast Name</copyright>\n <generator>n8n.io - Convert PDF Articles to Podcast</generator>\n <lastBuildDate>${pubDate}</lastBuildDate>\n\n <!-- Episode Item -->\n <item>\n <title>${docTitle} - Complete (${totalEpisodes} sections)</title>\n <description><![CDATA[\n 📚 Complete audio edition of ${docTitle}\n\n 🎯 What's inside:\n ${episodeDescription}\n\n ⏱️ Duration: ${durationFormatted}\n 📄 Sections: ${totalEpisodes}\n 🗓️ Published: ${new Date().toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })}\n ]]></description>\n <enclosure url=\"${r2Mp3Url}\" length=\"${fileSize}\" type=\"audio/mpeg\"/>\n <guid isPermaLink=\"false\">${fileName}</guid>\n <pubDate>${pubDate}</pubDate>\n\n <!-- iTunes Episode Metadata -->\n <itunes:title>${docTitle} - Complete Edition</itunes:title>\n <itunes:author>Your Name</itunes:author>\n <itunes:duration>${durationFormatted}</itunes:duration>\n <itunes:summary><![CDATA[Complete audio version with ${totalEpisodes} sections. ${episodeDescription.substring(0, 150)}...]]></itunes:summary>\n <itunes:subtitle>${totalEpisodes} sections • ${durationFormatted}</itunes:subtitle>\n <itunes:image href=\"${PODCAST_ARTWORK_URL}\"/>\n <itunes:episodeType>full</itunes:episodeType>\n <itunes:explicit>no</itunes:explicit>\n\n <!-- Rich Content -->\n <content:encoded><![CDATA[\n <h2>🎧 ${docTitle} - Complete Audio Edition</h2>\n <p><strong>Duration:</strong> ${durationFormatted} | <strong>Sections:</strong> ${totalEpisodes} | <strong>Size:</strong> ${Math.round(fileSize / 1024 / 1024 * 100) / 100} MB</p>\n <hr/>\n <h3>📑 Sections Included:</h3>\n ${episodeDescriptionHTML}\n <hr/>\n <p><em>Generated automatically by Convert PDF Articles to Podcast workflow</em></p>\n ]]></content:encoded>\n </item>\n\n </channel>\n</rss>`;\n\n// Convert XML to base64 for binary storage\nconst rssBase64 = Buffer.from(rssXml, 'utf-8').toString('base64');\n\n// Return data with enhanced metadata\nreturn {\n json: {\n ...mp3Data,\n r2Mp3Url: r2Mp3Url,\n rssGenerated: true,\n rssFileName: RSS_FILENAME,\n estimatedDuration: durationFormatted,\n podcastArtwork: PODCAST_ARTWORK_URL\n },\n binary: {\n rssFile: {\n data: rssBase64,\n mimeType: 'application/rss+xml',\n fileName: RSS_FILENAME,\n fileExtension: 'xml'\n }\n }\n};\n"
},
"typeVersion": 2
},
{
"id": "dd7a9af8-5031-48a7-a9b8-9658941792d8",
"name": "RSS in R2 hochladen",
"type": "n8n-nodes-cloudflare-r2-storage.cloudflareR2Storage",
"position": [
832,
384
],
"parameters": {
"objectKey": "={{ $('⚙️ Workflow Config').first().json.RSS_FILENAME }}",
"bucketName": "={{ $('⚙️ Workflow Config').first().json.R2_BUCKET }}",
"contentType": "application/rss+xml",
"binaryPropertyName": "rssFile"
},
"typeVersion": 1
},
{
"id": "b600862c-f392-49da-b9dd-4fe550c76615",
"name": "Monatliche Nutzung aktualisieren",
"type": "n8n-nodes-base.code",
"position": [
1040,
384
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// Get data from Merge node (has original MP3 data)\nconst data = $('Merge').first().json;\nconst totalChars = data.totalCharacters || data.charCount || 0;\nconst totalEpisodes = data.totalEpisodes || 1;\n\nconst now = new Date();\nconst monthKey = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;\n\nconst monthlyUsage = $getWorkflowStaticData('global');\n\nif (!monthlyUsage[monthKey]) {\n monthlyUsage[monthKey] = {\n charCount: 0,\n requestCount: 0,\n lastUpdated: new Date().toISOString()\n };\n}\n\nmonthlyUsage[monthKey].charCount += totalChars;\nmonthlyUsage[monthKey].requestCount += totalEpisodes;\nmonthlyUsage[monthKey].lastUpdated = new Date().toISOString();\n\nreturn {\n json: {\n ...data,\n updatedUsage: {\n monthKey,\n totalChars: monthlyUsage[monthKey].charCount,\n totalRequests: monthlyUsage[monthKey].requestCount,\n lastUpdated: monthlyUsage[monthKey].lastUpdated,\n percentUsed: Math.round((monthlyUsage[monthKey].charCount / 950000) * 100)\n }\n }\n};"
},
"typeVersion": 2
},
{
"id": "f532fce3-46d4-42b3-8ae4-645805f90cd1",
"name": "Für E-Mail aggregieren",
"type": "n8n-nodes-base.code",
"position": [
1248,
384
],
"parameters": {
"jsCode": "// Get data from Merge node (has original MP3 data)\nconst data = $('Merge').first().json;\nconst episodes = data.episodes || [];\nconst totalChars = data.totalCharacters || data.charCount || 0;\nconst totalEpisodes = data.totalEpisodes || episodes.length;\nconst mp3FileSize = data.fileSize || 0;\nconst mp3FileName = data.fileName || 'Podcast.mp3';\nconst mp3FileNameWithoutExt = mp3FileName.replace(/\\.mp3$/i, '').replace(/_Complete_\\d{4}-\\d{2}-\\d{2}$/, '');\n\n// Get R2 RSS feed URL from standalone config node\nconst config = $('⚙️ Workflow Config').first().json;\nconst R2_PUBLIC_URL = config.R2_PUBLIC_URL;\nconst RSS_FILENAME = config.RSS_FILENAME;\nconst rssFeedUrl = `${R2_PUBLIC_URL}/${RSS_FILENAME}`;\n\n// Get usage data\nconst usageData = data.updatedUsage || {\n totalChars: totalChars,\n totalRequests: totalEpisodes,\n percentUsed: Math.round((totalChars / 950000) * 100)\n};\n\n// Build episode list HTML\nconst episodeListHTML = episodes.map((ep, idx) =>\n `<li><strong>${ep.title}</strong><br>Episode ${ep.number} | ${ep.charCount.toLocaleString()} characters</li>`\n).join('\\n');\n\n// Build email HTML\nconst emailHTML = `\n<h2>🎧 New Podcast Episode Available!</h2>\n\n<p><strong>Total Sections:</strong> ${totalEpisodes} (stitched into 1 complete file)</p>\n<p><strong>Total Characters:</strong> ${totalChars.toLocaleString()}</p>\n<p><strong>File Size:</strong> ${Math.round(mp3FileSize / 1024 / 1024 * 100) / 100} MB</p>\n<p><strong>File Name:</strong> ${mp3FileNameWithoutExt}</p>\n\n<hr>\n\n<h3>📋 Sections Created:</h3>\n<ul>\n${episodeListHTML}\n</ul>\n\n<hr>\n\n<h3>📡 Subscribe to Podcast Feed</h3>\n<p>Copy this RSS feed URL and paste it into your podcast app:</p>\n<p style=\"background-color: #f0f0f0; padding: 10px; font-family: monospace; word-break: break-all; font-size: 12px;\">${rssFeedUrl}</p>\n<p><em>Supported apps: Apple Podcasts, Spotify, Pocket Casts, Overcast, Castro, and most other podcast apps</em></p>\n\n<hr>\n\n<h3>📊 Monthly Usage</h3>\n<p><strong>Used:</strong> ${usageData.totalChars.toLocaleString()} / 950,000 characters (${usageData.percentUsed}%)</p>\n<p><strong>Total Episodes This Month:</strong> ${usageData.totalRequests}</p>\n\n<hr>\n\n<p style=\"color: #666;\">✅ All ${totalEpisodes} sections are stitched into 1 MP3 file hosted on Cloudflare R2!</p>\n<p style=\"color: #666;\">✅ RSS feed generated and ready for subscription!</p>\n`;\n\nreturn {\n json: {\n totalEpisodes: totalEpisodes,\n totalCharacters: totalChars,\n rssFeedUrl: rssFeedUrl,\n emailSubject: `New Podcast: ${totalEpisodes} Sections from ${mp3FileNameWithoutExt}`,\n emailHTML: emailHTML,\n usageData: usageData\n }\n};"
},
"typeVersion": 2
},
{
"id": "67f70d82-cc76-471f-8cbd-986f24e3e84b",
"name": "E-Mail senden",
"type": "n8n-nodes-base.emailSend",
"position": [
1456,
384
],
"parameters": {
"html": "={{ $json.emailHTML }}",
"options": {},
"subject": "={{ $json.emailSubject }}",
"toEmail": "your-email@example.com",
"fromEmail": "noreply@yourdomain.com",
"emailFormat": "html"
},
"typeVersion": 2
},
{
"id": "9c67b04e-d782-49dd-b577-f6abf22ad944",
"name": "Zusammenführen",
"type": "n8n-nodes-base.merge",
"position": [
256,
384
],
"parameters": {
"mode": "combine",
"options": {},
"combineBy": "combineByPosition"
},
"typeVersion": 3.2
},
{
"id": "ad592fb6-b1b5-4af6-a572-7e2994512bea",
"name": "Notizzettel",
"type": "n8n-nodes-base.stickyNote",
"position": [
-560,
-80
],
"parameters": {
"color": 2,
"width": 520,
"height": 880,
"content": "# Convert PDF Articles to Podcast\n\n**Transform any PDF into an audio podcast automatically!**\n\nThis workflow extracts text from PDFs, converts it to natural-sounding speech using Google Cloud Text-to-Speech, stores files in cloud storage, and generates an RSS feed compatible with all major podcast apps.\n\n## 🎯 Use Case\nPerfect for consuming long-form content (articles, research papers, study materials) while commuting, exercising, or multitasking. Turn your reading list into a personal podcast feed.\n\n## 🔧 How it Works\n1. Upload a PDF article via web form\n2. Extract and split text into sections\n3. Convert text to speech using Google TTS API\n4. Stitch audio sections into complete MP3 file\n5. Upload MP3 to Cloudflare R2 storage\n6. Generate iTunes-compatible RSS feed\n7. Upload RSS feed to R2\n8. Send email notification with RSS feed link\n9. Subscribe to feed in any podcast app!\n\n## 📋 Requirements\n* **Google Cloud Text-to-Speech API** - Free tier: 1M characters/month (WaveNet voices) - [Setup Guide](https://cloud.google.com/text-to-speech/docs)\n* **Cloudflare R2 Object Storage** - Free tier: 10GB storage, unlimited egress - [Setup Guide](https://developers.cloudflare.com/r2/)\n* **Cloudflare R2 Storage Community Node** - Install via: Settings → Community Nodes → Install → `n8n-nodes-cloudflare-r2-storage`\n* **Email Service** - SMTP or OAuth credentials (Gmail, SendGrid, etc.)\n\n## ⚙️ Configuration\nUpdate the **Workflow Config** node with:\n- R2 bucket name and public URL\n- RSS feed filename\n- Podcast artwork URL\n- Your email address\n\nSee full setup instructions in the [GitHub repo](https://github.com/devdutta/PDF-to-Podcast---N8N)!\n"
},
"typeVersion": 1
}
],
"settings": {
"executionOrder": "v1"
},
"connections": {
"9c67b04e-d782-49dd-b577-f6abf22ad944": {
"main": [
[
{
"node": "72ae9ece-7ea5-48f8-969c-79f70376ff1b",
"type": "main",
"index": 0
}
]
]
},
"9789ab0b-b679-4314-a814-5407a42fb7d1": {
"main": [
[
{
"node": "dd7a9af8-5031-48a7-a9b8-9658941792d8",
"type": "main",
"index": 0
}
]
]
},
"ce537777-d89c-445b-b1ed-199beac096ce": {
"main": [
[
{
"node": "7948efd4-3d73-4496-95fa-12d25ff97fe0",
"type": "main",
"index": 0
}
]
]
},
"4f7236ac-852d-4c6f-9393-1fd6ed1c5610": {
"main": [
[
{
"node": "b533785e-e3bb-4bc6-b3a4-ab1f452ae330",
"type": "main",
"index": 0
}
]
]
},
"72ae9ece-7ea5-48f8-969c-79f70376ff1b": {
"main": [
[
{
"node": "9789ab0b-b679-4314-a814-5407a42fb7d1",
"type": "main",
"index": 0
}
]
]
},
"dd7a9af8-5031-48a7-a9b8-9658941792d8": {
"main": [
[
{
"node": "b600862c-f392-49da-b9dd-4fe550c76615",
"type": "main",
"index": 0
}
]
]
},
"f532fce3-46d4-42b3-8ae4-645805f90cd1": {
"main": [
[
{
"node": "67f70d82-cc76-471f-8cbd-986f24e3e84b",
"type": "main",
"index": 0
}
]
]
},
"b533785e-e3bb-4bc6-b3a4-ab1f452ae330": {
"main": [
[
{
"node": "aac02256-376d-421d-897b-06bdfd1b9494",
"type": "main",
"index": 0
}
]
]
},
"b600862c-f392-49da-b9dd-4fe550c76615": {
"main": [
[
{
"node": "f532fce3-46d4-42b3-8ae4-645805f90cd1",
"type": "main",
"index": 0
}
]
]
},
"3a32b49b-bcac-4a61-b405-2f5d412a1191": {
"main": [
[
{
"node": "ce537777-d89c-445b-b1ed-199beac096ce",
"type": "main",
"index": 0
}
]
]
},
"0cb6f54b-34b7-449c-b42c-1a8eb341001a": {
"main": [
[
{
"node": "4f7236ac-852d-4c6f-9393-1fd6ed1c5610",
"type": "main",
"index": 0
},
{
"node": "8616c449-1148-438e-b1a0-57dcc9b6caed",
"type": "main",
"index": 0
}
]
]
},
"8616c449-1148-438e-b1a0-57dcc9b6caed": {
"main": [
[
{
"node": "9c67b04e-d782-49dd-b577-f6abf22ad944",
"type": "main",
"index": 1
}
]
]
},
"7948efd4-3d73-4496-95fa-12d25ff97fe0": {
"main": [
[
{
"node": "9b9c9e38-5b5a-491f-b3b7-5d0d39c73ad5",
"type": "main",
"index": 0
}
]
]
},
"aac02256-376d-421d-897b-06bdfd1b9494": {
"main": [
[
{
"node": "3a32b49b-bcac-4a61-b405-2f5d412a1191",
"type": "main",
"index": 0
}
]
]
},
"9b9c9e38-5b5a-491f-b3b7-5d0d39c73ad5": {
"main": [
[
{
"node": "9c67b04e-d782-49dd-b577-f6abf22ad944",
"type": "main",
"index": 0
}
]
]
}
}
}Wie verwende ich diesen Workflow?
Kopieren Sie den obigen JSON-Code, erstellen Sie einen neuen Workflow in Ihrer n8n-Instanz und wählen Sie "Aus JSON importieren". Fügen Sie die Konfiguration ein und passen Sie die Anmeldedaten nach Bedarf an.
Für welche Szenarien ist dieser Workflow geeignet?
Experte
Ist es kostenpflichtig?
Dieser Workflow ist völlig kostenlos. Beachten Sie jedoch, dass Drittanbieterdienste (wie OpenAI API), die im Workflow verwendet werden, möglicherweise kostenpflichtig sind.
Verwandte Workflows
Dev Dutta
@devduttaData and Product Analytics| Business Intelligence| Data Governance| Tech. Strategy| Driving Digital Transformation Programs| Process Automation | Agentic AI
Diesen Workflow teilen