This workflow corresponds to n8n.io template #6945 — we link there as the canonical source.
This workflow follows the Agent → HTTP Request recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"id": "4zCVyhXKummU8dD4",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Daily Sports Digest",
"tags": [],
"nodes": [
{
"id": "e01655d1-9722-4630-a7d6-4425282ace11",
"name": "Google Gemini Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"position": [
-2500,
-280
],
"parameters": {
"options": {},
"modelName": "models/gemini-2.5-pro"
},
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "3271e00c-bf97-4d63-b1ec-8e33c63eaf29",
"name": "Google Gemini Chat Model1",
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"position": [
-2320,
620
],
"parameters": {
"options": {},
"modelName": "models/gemini-2.5-pro"
},
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "e34ac6ba-25c6-4941-9e53-31ff3bb00801",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-4740,
-600
],
"parameters": {
"width": 420,
"height": 1240,
"content": "## \ud83c\udfa7 Daily RSS Digest & Podcast Generation\n\n**Convert RSS Feeds into a Podcast with Google Gemini, Kokoro TTS, and FFmpeg:** This workflow automates the creation of a daily sports podcast from your favorite news sources. It fetches articles, uses AI to write a digest and a two-person dialogue, and produces a single, merged audio file with KOKORO TTS ready for listening.\n\n## \u2728 How it works:\n\n### \ud83d\udcf0 Fetch & Filter Daily News: The workflow triggers daily, fetches articles from your chosen RSS feeds, and filters them to keep only the most recent content.\n\n### \u270d\ufe0f Generate AI Digest & Script: \nUsing Google Gemini, it first creates a written summary of the day's news. A second AI agent then transforms this news into an engaging, conversational podcast script between two distinct AI speakers.\n\n### \ud83d\udde3\ufe0f Generate Voices in Chunks: \nThe script is split into individual lines of dialogue. The workflow then loops through each line, calling a Text-to-Speech (TTS) API to generate a separate audio file (an MP3 chunk) for each part of the conversation.\n\n### \ud83c\udf9b\ufe0f Merge Audio with FFmpeg: \nAfter all the audio chunks are created and saved locally, a command-line script generates a list of all the files and uses FFmpeg to losslessly merge them into a single, seamless MP3 file. All temporary files are then deleted.\n\n### \ud83d\udce4 Send the Final Podcast: \nThe final, merged MP3 is read from the server and delivered directly to your Telegram chat with a dynamic, dated filename.\n\n### You can modify:\n- \ud83d\udcf0 The RSS Feeds to any news source you want.\n- \ud83e\udd16 The AI Prompts to change the tone, language, or style of the digest and podcast.\n- \ud83c\udf99\ufe0f The TTS Voices used for the two speakers.\n- \ud83d\udceb The Final Delivery Method (e.g., send to Discord, save to Google Drive, etc.).\n\n\nPerfect for creating a personalized, hands-free news briefing to listen to on your commute.\n\n**Inspired by:** https://n8n.io/workflows/6523-convert-newsletters-into-ai-podcasts-with-gpt-4o-mini-and-elevenlabs/"
},
"typeVersion": 1
},
{
"id": "10c71894-9b5c-4bd0-a618-57ca8161275b",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-4300,
-600
],
"parameters": {
"color": 7,
"width": 1352,
"height": 1244,
"content": "## \ud83d\udcf0 Step 1: Fetch & Filter Daily News\n\nThis section acts as the data pipeline. It triggers once a day, fetches the latest articles from multiple RSS feeds, merges them, and filters the list to keep only articles published on the previous calendar day. This provides a clean, relevant dataset for the AI.\n\n### \u2705 Expected Output \nA clean list of news articles, each containing a title, content, link, and a standardized date, ready to be processed by the AI.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n### \ud83e\udd16 Logic Explained:\nThe crucial step is the Filter for Yesterday node. It keeps only articles published on the previous calendar day. It does this with two conditions:\n**1. is greater than: The article's date must be after midnight at the start of yesterday.\n2. is less than: The article's date must be before the time scheduled to running.**\n\nThis ensures the AI always has a fresh, consistent set of news to work with.\n\n### \u270d\ufe0f How to Customize:\n- Trigger: Change the time or frequency in the `Daily Trigger` node.\n- News Sources: Change the URLs in the Fetch... News nodes to any RSS feed you want. Add or remove feeds as needed, connecting them to the Merge News Sources node.\n"
},
"typeVersion": 1
},
{
"id": "a9078490-2bd4-4b5e-ada1-ee67e147d139",
"name": "Daily Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-4240,
-100
],
"parameters": {
"rule": {
"interval": [
{
"triggerAtHour": 8
}
]
}
},
"typeVersion": 1.2
},
{
"id": "0af5a33b-e2e4-4fc3-95c4-a5c24c6b2ab5",
"name": "Fetch RSS 1: Folha de SP",
"type": "n8n-nodes-base.rssFeedRead",
"position": [
-3900,
-260
],
"parameters": {
"url": "https://feeds.folha.uol.com.br/esporte/rss091.xml",
"options": {}
},
"typeVersion": 1.1
},
{
"id": "84cc82a2-937f-4b7f-801c-045fb7e164cf",
"name": "Fetch RSS 2: GE",
"type": "n8n-nodes-base.rssFeedRead",
"position": [
-3900,
40
],
"parameters": {
"url": "https://ge.globo.com/rss/ge/",
"options": {}
},
"typeVersion": 1.2
},
{
"id": "5b1afc5f-829b-4ee2-866e-4bf045631f43",
"name": "Clean Up Fields",
"type": "n8n-nodes-base.set",
"position": [
-3640,
-260
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "a0229879-764d-455d-9064-2d939d7e5701",
"name": "title",
"type": "string",
"value": "={{ $json.title.replace(/\\[PACK\\].*/, \"\").replace(/\\[.*?\\]/g, \"\").trim() }}"
},
{
"id": "2da9330c-e39f-4515-b737-d14f3c4aeb8b",
"name": "pubDate",
"type": "string",
"value": "={{ $json.pubDate }}"
},
{
"id": "c7b0f3d6-e2bb-48a7-9911-edcd44700868",
"name": "link",
"type": "string",
"value": "={{ $json.link.replace(/\\/torrent\\/download\\/(\\d+)\\..*/, \"/torrents/$1\") }}"
},
{
"id": "05d172b5-3201-450d-b02f-fc9b649664f0",
"name": "content",
"type": "string",
"value": "={{ $json.content }}"
},
{
"id": "db80b578-d9b4-40fd-bbe1-6e7615a27ce3",
"name": "isoDate",
"type": "number",
"value": "={{ new Date($json.isoDate).getTime() }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "0d76fc33-7930-4ab8-b363-b86e1e5f6889",
"name": "Clean Up Fields1",
"type": "n8n-nodes-base.set",
"position": [
-3640,
40
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "a0229879-764d-455d-9064-2d939d7e5701",
"name": "title",
"type": "string",
"value": "={{ $json.title.replace(/\\[PACK\\].*/, \"\").replace(/\\[.*?\\]/g, \"\").trim() }}"
},
{
"id": "2da9330c-e39f-4515-b737-d14f3c4aeb8b",
"name": "pubDate",
"type": "string",
"value": "={{ $json.pubDate }}"
},
{
"id": "c7b0f3d6-e2bb-48a7-9911-edcd44700868",
"name": "link",
"type": "string",
"value": "={{ $json.link.replace(/\\/torrent\\/download\\/(\\d+)\\..*/, \"/torrents/$1\") }}"
},
{
"id": "05d172b5-3201-450d-b02f-fc9b649664f0",
"name": "content",
"type": "string",
"value": "={{ $json.content }}"
},
{
"id": "db80b578-d9b4-40fd-bbe1-6e7615a27ce3",
"name": "isoDate",
"type": "number",
"value": "={{ new Date($json.isoDate).getTime() }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "69db334b-7681-4174-abaf-f745306d28f4",
"name": "Merge News Sources",
"type": "n8n-nodes-base.merge",
"position": [
-3380,
-100
],
"parameters": {},
"typeVersion": 3
},
{
"id": "56d67b29-8950-49f8-bdea-d8cd5461c8da",
"name": "Filter for Last 24hrs' News",
"type": "n8n-nodes-base.filter",
"position": [
-3100,
-100
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "c590146a-caae-495c-a933-37864e921876",
"operator": {
"type": "number",
"operation": "gt"
},
"leftValue": "={{ $json.isoDate }}",
"rightValue": "={{ (new Date()).setHours(0, 0, 0, 0) - 24 * 60 * 60 * 1000 }}"
},
{
"id": "e7cf09fb-af35-495d-a840-341f8d0ddcd8",
"operator": {
"type": "number",
"operation": "lt"
},
"leftValue": "={{ $json.isoDate }}",
"rightValue": "={{ (new Date()).setHours(0, 0, 0, 0) }}"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "f597998a-7621-49ce-9933-6db94c53325e",
"name": "Prepare Data for AI",
"type": "n8n-nodes-base.code",
"position": [
-2820,
-100
],
"parameters": {
"jsCode": "// This code gets all the news items that passed the filter.\nconst allItems = $input.all();\n\n// We will format each news item with its title, content, and a link.\nconst formattedNews = allItems.map(item => {\n // Get the title, content, and link from the JSON data of each item.\n const title = item.json.title;\n const content = item.json.content;\n const link = item.json.link;\n\n // Return a clean, formatted string for each article.\n return `\n---\nTitle: ${title}\nContent: ${content}\nLink: ${link}\n---\n `;\n});\n\n// --- NEW CODE ADDED BELOW ---\n// Get today's date and format it beautifully.\n// It will look like: \"July 31, 2025\"\nconst digestDate = $now.setZone('America/Sao_Paulo').toFormat('MMMM d, yyyy');\n\n// Join all the formatted news items into a single block of text\n// and return it as an object along with our new date string.\nreturn {\n allNews: formattedNews.join('\\n'),\n digestDate: digestDate\n};"
},
"typeVersion": 2
},
{
"id": "db56296f-138c-4441-9593-a2392efef7fd",
"name": "Create Temp Directory",
"type": "n8n-nodes-base.executeCommand",
"position": [
-2540,
380
],
"parameters": {
"command": "mkdir -p /tmp/dailydigest"
},
"typeVersion": 1
},
{
"id": "a0290570-125b-4e68-a06c-f89cc83efd10",
"name": "Generate Text Digest",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
-2500,
-520
],
"parameters": {
"text": "=**ROLE & GOAL:**\nYou are a witty and engaging sports journalist. Your goal is to create a \"Daily Sports Digest\" in English that is informative and fun to read, summarizing sports news from Brazil.\n\n**CONTEXT:**\nYou will receive a block of text with news articles written in Portuguese and a formatted string for today's date.\n\n**CRITICAL RULES:**\n1. **Language:** Your entire output MUST be in English.\n2. **Source Integrity:** Use ONLY the information from the provided text. DO NOT add external information or speculate.\n3. **Markdown:** Your entire output must use MarkdownV2 for formatting.\n\n**OUTPUT STRUCTURE:**\n\n**1. Title:**\n- Use the provided date string to create a top-level heading like this: `# Daily Sports Digest: {{ $('Prepare Data for AI').item.json.digestDate }}`\n\n**2. Introduction:**\n- On the next line, start with a single, engaging paragraph (2-3 sentences) that gives a \"big picture\" summary of the day's main news or overall theme. Use a \"newspaper\" emoji \ud83d\udcf0 at the beginning.\n\n**3. Football Section:**\n- Create a main heading: `## \u26bd Football Focus`\n- If there is enough variety, categorize the football news into subheadings like `### \ud83c\udde7\ud83c\uddf7 Brazilian Clubs`, `### \ud83c\udf0e International`, or `### \ud83c\udfc6 Tournaments`. Use your best judgment based on the articles provided.\n\n**4. Other Sports Section:**\n- If there are articles on other sports (Motorsport, Basketball, etc.), group them all under a single heading: `## \u26a1 Around the Horn`.\n\n**5. News Item Format (for every single article):**\n- Start the line with a single, relevant emoji (e.g., investigatory nature of a story).\n- Translate the original title into a concise and accurate English headline. Display this new English headline as a clickable link to its URL.\n- On the **next line**, write your concise 1-2 sentence summary in English.\n\n**EXAMPLE of the required News Item format:**\nIf you receive: `Title: AFA e River Plate criticam aumento de imposto a clubes na Argentina\\nLink: http://example.com/news-link`\nYou must format it like this: `[AFA and River Plate Criticize Tax Increase on Clubs in Argentina](http://example.com/news-link)\\nThe Argentine Football Association and River Plate have criticized a government measure to increase taxes on football clubs.`\n\n---\n**Here is the block of text with today's articles:**\n{{ $('Prepare Data for AI').item.json.allNews }}",
"options": {},
"promptType": "define"
},
"typeVersion": 2
},
{
"id": "08b47cef-36e7-4491-8c9d-8c1bf674f474",
"name": "Send Text Digest",
"type": "n8n-nodes-base.telegram",
"position": [
-2040,
-520
],
"parameters": {
"text": "={{ $json.output }}",
"chatId": "[YOUR_TELEGRAM_CHAT_ID]",
"additionalFields": {
"parse_mode": "Markdown"
}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "cf8d7651-4b2b-4ede-9fff-c7ca9fa6b248",
"name": "Generate Podcast Script",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
-2320,
380
],
"parameters": {
"text": "=You are the scriptwriter of a podcast that transforms dense written content into a lively, natural conversation between two AI speakers, `voice1` and `voice2`.\n\nYour task is to turn the following newsletter content into a **realistic audio dialogue**. The conversation should be fluid, informal, and engaging \u2014 similar in tone and structure to how NotebookLM rewrites long documents as discussions. It must sound like two well-informed people exchanging ideas, not like a text being read aloud.\n\n### Roles\n\n- **voice1**: Curious, expressive, casual, often injects humor or everyday references. Tends to ask questions, react with surprise or amusement, and bring lightness to the discussion.\n- **voice2**: Analytical, composed, insightful. Adds perspective, context, and a slightly ironic or dry sense of humor. Offers clarity without sounding robotic.\n\nUse realistic, human-like phrasing with brief interjections (`\"Right?\"`, `\"Let me stop you there\"`, `\"That's exactly it\"`). Use `<break time=\"1.5s\" />` tags occasionally to simulate natural pauses.\n\n### Structure\n\n1. **Introduction**: Set the scene naturally. Briefly introduce what the episode is about based on the content, without listing or labeling sections. Present `voice1` and `voice2` through dialogue, not narration.\n2. **Content Breakdown**: For each key idea or section from the newsletter:\n - Paraphrase the content in spoken language.\n - Embed the headline or theme organically in the conversation.\n - Include personal reactions, examples, and small tangents to make it relatable.\n - Open loops by teasing questions or ideas that will be answered later in the conversation.\n - Maintain curiosity and variety in tone and rhythm.\n3. **Closing**: End warmly and casually, with a brief comment on what stood out or what\u2019s coming next (no need for formal farewells).\n\n### Requirements\n\n- The script must be at least **ten thousand characters** (about 15 minutes of speech).\n- Use **commas** to separate items in a list, not periods.\n- Format the output as a single uninterrupted block of text with clear speaker tags:\n \nvoice1: \u2026\nvoice2: \u2026\n\nYou will be given a newsletter input under this key:\n\n{{ $('Prepare Data for AI').item.json.allNews }}\n\nGenerate only the final dialogue script \u2014 no explanations, bullet points, or headings. Just the conversation in English.\n\n\n",
"options": {},
"promptType": "define"
},
"typeVersion": 2
},
{
"id": "04689446-91a9-4ba2-b76f-db126e150a47",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2900,
-600
],
"parameters": {
"color": 7,
"width": 1112,
"height": 1604,
"content": "## \u270d\ufe0f Step 2: Generate AI Content (Digest & Script)\n### \u2705 Expected Output:\nA formatted text message sent to Telegram and a \ntwo-person podcast script.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n### \ud83e\udd16 Logic Explained:\nThis section has two parallel branches:\n\n**1. Text Digest:** The `Generate Text Digest` agent creates a written summary of the news, which is immediately sent to Telegram via the `Send Text Digest` node. This provides a quick, readable update.\n\n**2. Podcast Script:** The `Generate Podcast Script` agent takes the same news data and, following a detailed prompt, writes a long-form conversational dialogue between two distinct AI speakers `(voice1 and voice2)`. The `Create Temp Directory` node runs first to ensure the folder for saving audio files `(/tmp/dailydigest)` exists on the server.\n\n### \u270d\ufe0f How to Customize:\n- AI Prompts: The core of this section is the prompts inside the `Generate Text Digest` and `Generate Podcast Script` nodes. **You can edit these extensively to change the tone, style, length, language, and format of the output.**\n- AI Model: Change the model used for generation in the corresponding `Google Gemini Chat Model` nodes.\n- Directory: If you change the temporary directory path in the Create Temp Directory node, **you must update it everywhere else in the workflow.**"
},
"typeVersion": 1
},
{
"id": "2a450954-0691-423a-b5c7-c8b9aac595ca",
"name": "Split Script by Speaker",
"type": "n8n-nodes-base.code",
"position": [
-1720,
380
],
"parameters": {
"jsCode": "/**\n * This Function node takes the script from the previous node\n * and splits it using \"voice1:\" and \"voice2:\" as delimiters.\n * Each resulting segment retains the respective identifier.\n */\n\nconst script = $input.first().json.output|| \"\";\n\n// Ensure consistent line breaks\nconst normalizedScript = script.replace(/\\r\\n/g, \"\\n\");\n\n// Split the script while keeping \"voice1:\" and \"voice2:\" in the result\nconst segments = normalizedScript.split(/(?=(?:voice1:|voice2:))/g).map(s => s.trim()).filter(Boolean);\n\n// Return one item per segment\nreturn segments.map(segment => {\n return {\n json: {\n segment\n }\n };\n});"
},
"typeVersion": 2
},
{
"id": "467b302f-98fd-4c81-9dbd-8a467469d057",
"name": "Loop Through Segments",
"type": "n8n-nodes-base.splitInBatches",
"position": [
-1500,
380
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "ff9fa0c1-cc63-4fac-8001-e095f8b6d9ae",
"name": "Clean Dialogue Segment",
"type": "n8n-nodes-base.code",
"position": [
-1320,
560
],
"parameters": {
"jsCode": "const paragraph = $input.first().json.segment; \nif (!paragraph) {\n throw new Error(\"No se encontr\u00f3 contenido de texto en el correo.\");\n}\n\nlet cleanedText = paragraph\n .replace(/\"/g, \"\")\n .replace(/\u201c/g, \"\")\n .replace(/\u201d/g, \"\");\n\ncleanedText = cleanedText.replace(/\\n/g, \"\");\n\nconsole.log(\"Texto limpio sin comillas ni saltos de l\u00ednea:\", cleanedText);\n\nreturn [{ json: { cleanedText } }];\n"
},
"typeVersion": 2
},
{
"id": "228ee637-49f9-4ecd-ac6d-f532a2aef401",
"name": "Route to Correct Voice",
"type": "n8n-nodes-base.if",
"position": [
-1100,
560
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "93609a28-55f5-439e-8238-a48375255f4f",
"operator": {
"type": "string",
"operation": "contains"
},
"leftValue": "={{ $json.cleanedText }}",
"rightValue": "voice1:"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "4857627b-027d-4270-8a48-3f2c4461d3a7",
"name": "Prepare Text for TTS",
"type": "n8n-nodes-base.code",
"position": [
-880,
560
],
"parameters": {
"jsCode": "// Get the text from the previous node.\nconst cleanedText = $input.first().json.cleanedText;\n\n// Safety check: Ensure the input is a string.\nif (typeof cleanedText !== \"string\") {\n throw new Error(\"Input 'cleanedText' must be a string.\");\n}\n\n// Debugging: Log the original text received by the node.\nconsole.log(\"Original text:\", JSON.stringify(cleanedText, null, 2));\n\n// Debugging: Check if the speaker tag exists before removal.\nif (cleanedText.includes(\"voice1:\")) {\n console.log(\"\u2705 'voice1:' detected in original text.\");\n} else {\n console.log(\"\u274c 'voice1:' NOT found in original text. Check input!\");\n}\n\n// This is the main action: Remove the speaker tag (e.g., \"voice1: \") and trim whitespace.\nconst modifiedString = cleanedText.replace(/\\bvoice1:\\s*/gi, \"\").trim();\n\n// Debugging: Log the text *after* modification to confirm it was removed.\nconsole.log(\"Modified text:\", JSON.stringify(modifiedString, null, 2));\n\n// Debugging: Final check to ensure the tag is gone.\nif (modifiedString.includes(\"voice1:\")) {\n console.log(\"\u274c 'voice1:' is still present in the modified text. The regex needs adjustment!\");\n} else {\n console.log(\"\u2705 'voice1:' removed successfully.\");\n}\n\n// Return the final, cleaned string for the TTS API.\nreturn [\n {\n json: {\n modifiedString\n }\n }\n];\n"
},
"typeVersion": 2
},
{
"id": "e43ecaff-904d-4039-b8f3-21680080ed99",
"name": "Prepare Text for TTS1",
"type": "n8n-nodes-base.code",
"position": [
-860,
800
],
"parameters": {
"jsCode": "const cleanedText = $input.first().json.cleanedText;\n\nif (typeof cleanedText !== \"string\") {\n throw new Error(\"cleanedText debe ser un string.\");\n}\n\nconsole.log(\"Texto original:\", JSON.stringify(cleanedText, null, 2));\n\nif (cleanedText.includes(\"voice2:\")) {\n console.log(\"\u2705 'voice2:' detectado en el texto original.\");\n} else {\n console.log(\"\u274c 'voice2:' NO encontrado en el texto original. \u00a1Revisar input!\");\n}\n\nconst modifiedString = cleanedText.replace(/\\bvoice2:\\s*/gi, \"\").trim();\n\nconsole.log(\"Texto modificado:\", JSON.stringify(modifiedString, null, 2));\n\nif (modifiedString.includes(\"voice2:\")) {\n console.log(\"\u274c 'voice2:' sigue presente en el texto modificado. \u00a1El regex debe ajustarse!\");\n} else {\n console.log(\"\u2705 'voice2:' eliminado correctamente.\");\n}\n\nreturn [\n {\n json: {\n modifiedString\n }\n }\n];"
},
"typeVersion": 2
},
{
"id": "66c339ee-6893-4f1e-ac56-f76a77d02591",
"name": "Generate Audio (Voice 1)",
"type": "n8n-nodes-base.httpRequest",
"position": [
-600,
560
],
"parameters": {
"url": "https://tts-kokoro.mfxikq.easypanel.host/api/v1/audio/speech",
"method": "POST",
"options": {},
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "model",
"value": "model_q8f16"
},
{
"name": "voice",
"value": "am_liam"
},
{
"name": "speed",
"value": "={{ 1 }}"
},
{
"name": "response_format",
"value": "mp3"
},
{
"name": "input",
"value": "={{ $json.modifiedString }}"
}
]
}
},
"notesInFlow": true,
"retryOnFail": true,
"typeVersion": 4.2
},
{
"id": "d725b21a-4b3e-444e-baf4-a08cd3a3d663",
"name": "Generate Audio (Voice 2)",
"type": "n8n-nodes-base.httpRequest",
"position": [
-600,
800
],
"parameters": {
"url": "https://tts-kokoro.mfxikq.easypanel.host/api/v1/audio/speech",
"method": "POST",
"options": {},
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "model",
"value": "model_q8f16"
},
{
"name": "voice",
"value": "af_heart"
},
{
"name": "speed",
"value": "={{ 1 }}"
},
{
"name": "response_format",
"value": "mp3"
},
{
"name": "input",
"value": "={{ $json.modifiedString }}"
}
]
}
},
"retryOnFail": true,
"typeVersion": 4.2
},
{
"id": "cfd32a14-4359-4025-9ed6-6b41bfd48432",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1760,
-600
],
"parameters": {
"color": 7,
"width": 1372,
"height": 1604,
"content": "## \u270d\ufe0f Step 3: Split Script & Generate Audio Chunks\n\n### \u2705 Expected Output:\nA series of individual items, each containing the binary data for one small MP3 audio chunk. These are now ready to be saved to the server.\n\n### \ud83e\udd16 Logic Explained:\nThis section is the core audio generation engine.\n\n**1. Split Script:** The `Split Script by Speaker` node takes the long dialogue from the AI and uses a regular expression to split it into an array of smaller text chunks, one for each line of dialogue.\n**2. Loop & Clean:** The workflow then loops through each chunk. The `Clean Dialogue Segment` and `Prepare Text for TTS` nodes remove any unwanted characters or speaker tags (like \"voice1:\") from the text to ensure it's clean for the TTS API.\n**3. Route & Generate:** The `Route to Correct Voice` node checks which speaker the line belongs to and sends it to the correct HttpRequest node. Each of these nodes is configured to call the TTS API with a different voice, generating a unique MP3 audio file for that specific line.\n\n### \ud83d\udde3\ufe0f TTS Service: `Kokoro`\nThis workflow uses two HttpRequest nodes to call the `Kokoro TTS API`. This service was chosen for its straightforward API and voice options.\n\nA brief explanation about the TTS setup:\n- Authentication: You must get your own API key from Kokoro. In both `HttpRequest` nodes (`Generate Audio (Voice 1)` and `Generate Audio (Voice 2)`), go to the Headers section and replace the placeholder API key in the `X-API-KEY` header.\n- Voices: To change the voices for the two speakers, modify the voice parameter in the Body of each HttpRequest node. You can find a list of available voices in the Kokoro API documentation\n\n### \u270d\ufe0f How to Customize:\n- Voices: The main customization here is in the `Generate Audio (Voice 1)` and `Generate Audio (Voice 2)` nodes. You can change the voice parameter in the request body to use any of the voices your TTS service provides.\n- Cleaning Logic: The Code nodes that clean the text can be adjusted if your AI's output changes or includes other unwanted characters."
},
"typeVersion": 1
},
{
"id": "877dd4d2-3096-448f-bb36-759ee1abb352",
"name": "Save Audio Chunk to Disk",
"type": "n8n-nodes-base.readWriteFile",
"position": [
-260,
360
],
"parameters": {
"options": {},
"fileName": "=/tmp/dailydigest_{{$itemIndex}}.mp3",
"operation": "write"
},
"typeVersion": 1
},
{
"id": "5bf9e496-fa0d-4d4c-8908-de01512fe4d8",
"name": "Generate FFmpeg Concat List",
"type": "n8n-nodes-base.code",
"position": [
-40,
360
],
"parameters": {
"jsCode": "/**\n * This Code node will:\n * 1. Gather all file paths from the incoming items (assuming each item has `item.json.filePath`).\n * 2. Build a single text string, each line in FFmpeg concat format: `file '/path/to/audio.mp3'`\n * 3. Convert that text to binary (Base64) so the next node (\"Write Binary File\") can save it as `concat_list.txt`.\n */\n\nconst items = $input.all();\n\n// Build the concat list\nlet concatListText = '';\n\nitems.forEach(item => {\n // The 'Save File' node outputs the path in item.json.filePath\n const filePath = item.json.filePath;\n if (filePath) {\n concatListText += `file '${filePath}'\\n`;\n }\n});\n\n// The 'Save concat_list' node expects binary data\nconst buffer = Buffer.from(concatListText, 'utf-8');\nconst base64Data = buffer.toString('base64');\n\nreturn [\n {\n json: {},\n binary: {\n data: {\n data: base64Data,\n mimeType: 'text/plain',\n fileName: 'concat_list.txt'\n }\n }\n }\n];"
},
"typeVersion": 2
},
{
"id": "73680e23-7e03-4125-a5f9-91defe784743",
"name": "Save Concat List to Disk",
"type": "n8n-nodes-base.readWriteFile",
"position": [
180,
360
],
"parameters": {
"options": {},
"fileName": "/tmp/dailydigest/concat_list.txt",
"operation": "write"
},
"typeVersion": 1
},
{
"id": "9b64cb66-f9be-449e-a12a-3e3e5504cadb",
"name": "Merge Audio & Clean Up",
"type": "n8n-nodes-base.executeCommand",
"position": [
400,
360
],
"parameters": {
"command": "ffmpeg -y -f concat -safe 0 -i /tmp/dailydigest/concat_list.txt -c copy /tmp/dailydigest/final_merged.mp3\n\nfind /tmp/dailydigest/ -type f ! -name \"final_merged.mp3\" -delete\n"
},
"typeVersion": 1
},
{
"id": "91281464-dc37-4ae0-94f2-bea3d1054fdf",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-360,
-600
],
"parameters": {
"color": 7,
"width": 932,
"height": 1264,
"content": "## \ud83c\udf9b\ufe0f Step 4: Save, Prepare, & Merge Audio\n\n### \u2705 Expected Output:\nA single item containing the path to the final merged MP3 file, ready to be read and sent.\n\n### \ud83e\udd16 Logic Explained:\nThis is the file management and assembly line of the workflow.\n\n**1. Save Chunks:** The `Save Audio Chunk to Disk` node takes each MP3 from the loop and saves it to the `/tmp/dailydigest/` directory with a unique, indexed filename (e.g., audio_0.mp3, audio_1.mp3).\n**2. Generate List:** After the loop finishes, the `Generate FFmpeg Concat List` node runs. It gathers the file paths of all the saved chunks and creates a simple text file `(concat_list.txt)` that acts as a playlist for FFmpeg.\n**3. Merge & Clean:** The `Merge Audio & Clean Up` node executes two commands. First, it runs ffmpeg, which reads the concat_list.txt and joins all the audio chunks into a single, final MP3 file. Second, it runs a find command to delete all the temporary audio chunks and the list file, keeping the server clean.\n\n### \u270d\ufe0f How to Customize:\n- The main customization is in the `Merge Audio & Clean Up` node. You can modify the ffmpeg command to change audio quality, add fades, or perform other advanced audio processing."
},
"typeVersion": 1
},
{
"id": "ad20440c-57dd-4e26-8505-13aa166cd274",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
600,
-600
],
"parameters": {
"color": 7,
"width": 632,
"height": 1264,
"content": "## \ud83d\udce4 Step 5: Read Merged Audio & Send Final Podcast\n\n### \u2705 Expected Output:\nA series of individual items, each containing the binary data for one small MP3 audio chunk. These are now ready to be saved to the server.\n\n### \ud83e\udd16 Logic Explained:\nThis is the final delivery stage.\n\n1. Read File: The `Read Final Merged MP3` node takes the path to the completed audio file created by `FFmpeg` and reads it from the server's disk into n8n's binary data format. This prepares it for upload.\n2. Send to Telegram: The `Send Podcast to Telegram` node takes this binary data and uploads it directly to your specified Telegram chat.\n\n### \u270d\ufe0f How to Customize:\n- Delivery Channel: You can replace the `Send Podcast to Telegram` node with any other node to change the final destination (e.g., Google Drive, Discord, Email).\n- Filename: **The filename that appears in Telegram is set dynamically in the Additional Fields section of the Telegram node. You can change the format of the date or the name of the file by editing the expression there.**"
},
"typeVersion": 1
},
{
"id": "aed39482-9644-4f38-93e6-4df6bb2d1742",
"name": "Read Final Merged MP3",
"type": "n8n-nodes-base.readWriteFile",
"position": [
720,
360
],
"parameters": {
"options": {},
"fileSelector": "/tmp/dailydigest/final_merged.mp3"
},
"typeVersion": 1
},
{
"id": "4069618f-f4e0-484b-92bc-39907a876896",
"name": "Send Podcast to Telegram",
"type": "n8n-nodes-base.telegram",
"position": [
1000,
360
],
"parameters": {
"chatId": "[YOUR_TELEGRAM_CHAT_ID]",
"operation": "sendAudio",
"binaryData": true,
"additionalFields": {
"fileName": "=Daily Digest - {{ $now.setZone('America/Sao_Paulo').toFormat('dd/LL/yyyy') }}.mp3"
}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
}
],
"active": true,
"settings": {
"executionOrder": "v1"
},
"versionId": "59a82666-de28-4e02-bf8e-936f8b628674",
"connections": {
"Daily Trigger": {
"main": [
[
{
"node": "Fetch RSS 1: Folha de SP",
"type": "main",
"index": 0
},
{
"node": "Fetch RSS 2: GE",
"type": "main",
"index": 0
}
]
]
},
"Clean Up Fields": {
"main": [
[
{
"node": "Merge News Sources",
"type": "main",
"index": 0
}
]
]
},
"Fetch RSS 2: GE": {
"main": [
[
{
"node": "Clean Up Fields1",
"type": "main",
"index": 0
}
]
]
},
"Clean Up Fields1": {
"main": [
[
{
"node": "Merge News Sources",
"type": "main",
"index": 1
}
]
]
},
"Merge News Sources": {
"main": [
[
{
"node": "Filter for Last 24hrs' News",
"type": "main",
"index": 0
}
]
]
},
"Prepare Data for AI": {
"main": [
[
{
"node": "Create Temp Directory",
"type": "main",
"index": 0
},
{
"node": "Generate Text Digest",
"type": "main",
"index": 0
}
]
]
},
"Generate Text Digest": {
"main": [
[
{
"node": "Send Text Digest",
"type": "main",
"index": 0
}
]
]
},
"Prepare Text for TTS": {
"main": [
[
{
"node": "Generate Audio (Voice 1)",
"type": "main",
"index": 0
}
]
]
},
"Create Temp Directory": {
"main": [
[
{
"node": "Generate Podcast Script",
"type": "main",
"index": 0
}
]
]
},
"Loop Through Segments": {
"main": [
[
{
"node": "Save Audio Chunk to Disk",
"type": "main",
"index": 0
}
],
[
{
"node": "Clean Dialogue Segment",
"type": "main",
"index": 0
}
]
]
},
"Prepare Text for TTS1": {
"main": [
[
{
"node": "Generate Audio (Voice 2)",
"type": "main",
"index": 0
}
]
]
},
"Read Final Merged MP3": {
"main": [
[
{
"node": "Send Podcast to Telegram",
"type": "main",
"index": 0
}
]
]
},
"Clean Dialogue Segment": {
"main": [
[
{
"node": "Route to Correct Voice",
"type": "main",
"index": 0
}
]
]
},
"Merge Audio & Clean Up": {
"main": [
[
{
"node": "Read Final Merged MP3",
"type": "main",
"index": 0
}
]
]
},
"Route to Correct Voice": {
"main": [
[
{
"node": "Prepare Text for TTS",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare Text for TTS1",
"type": "main",
"index": 0
}
]
]
},
"Generate Podcast Script": {
"main": [
[
{
"node": "Split Script by Speaker",
"type": "main",
"index": 0
}
]
]
},
"Split Script by Speaker": {
"main": [
[
{
"node": "Loop Through Segments",
"type": "main",
"index": 0
}
]
]
},
"Fetch RSS 1: Folha de SP": {
"main": [
[
{
"node": "Clean Up Fields",
"type": "main",
"index": 0
}
]
]
},
"Generate Audio (Voice 1)": {
"main": [
[
{
"node": "Loop Through Segments",
"type": "main",
"index": 0
}
]
]
},
"Generate Audio (Voice 2)": {
"main": [
[
{
"node": "Loop Through Segments",
"type": "main",
"index": 0
}
]
]
},
"Google Gemini Chat Model": {
"ai_languageModel": [
[
{
"node": "Generate Text Digest",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Save Audio Chunk to Disk": {
"main": [
[
{
"node": "Generate FFmpeg Concat List",
"type": "main",
"index": 0
}
]
]
},
"Save Concat List to Disk": {
"main": [
[
{
"node": "Merge Audio & Clean Up",
"type": "main",
"index": 0
}
]
]
},
"Google Gemini Chat Model1": {
"ai_languageModel": [
[
{
"node": "Generate Podcast Script",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Filter for Last 24hrs' News": {
"main": [
[
{
"node": "Prepare Data for AI",
"type": "main",
"index": 0
}
]
]
},
"Generate FFmpeg Concat List": {
"main": [
[
{
"node": "Save Concat List to Disk",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
googlePalmApitelegramApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow automates the creation of a daily sports podcast from your favorite news sources. It fetches articles, uses AI to write a digest and a two-person dialogue, and produces a single, merged audio file with KOKORO TTS ready for listening.
Source: https://n8n.io/workflows/6945/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Scale short-form content without scaling your team. This template turns idea discovery → prompt & text generation → Veo-3 video rendering → vertical formatting → multi-channel publishing into one cohe
Instead of manually checking separate apps for your calendar, weather, and news each morning, this workflow consolidates the most important information into a single, convenient audio briefing. The "G
This workflow creates a multi-talented AI assistant named Simran that interacts with users via Telegram. It can handle text and voice messages, understand the user's intent, and perform various tasks.
RAG CHATBOT Main. Uses telegram, telegramTrigger, lmChatOpenAi, n8n-nodes-mcp. Event-driven trigger; 87 nodes.
Automatically scan major financial newswires for biotech catalyst events, score them with AI sentiment analysis, and surface ranked trade candidates — all without manual monitoring.