Modèle de conversion de communiqué de presse en audio v3

Avancé

Ceci est unContent Creation, Multimodal AIworkflow d'automatisation du domainecontenant 23 nœuds.Utilise principalement des nœuds comme If, Code, Gmail, HttpRequest, GmailTrigger. Utiliser GPT-4o Mini et ElevenLabs pour convertir des bulletins d'actualités en podcasts IA

Prérequis
  • Compte Google et informations d'identification Gmail API
  • Peut nécessiter les informations d'identification d'authentification de l'API cible
  • Clé API OpenAI
Aperçu du workflow
Visualisation des connexions entre les nœuds, avec support du zoom et du déplacement
Exporter le workflow
Copiez la configuration JSON suivante dans n8n pour importer et utiliser ce workflow
{
  "id": "kmIE2vOxAQCnkYZl",
  "meta": {
    "instanceId": "e3b8c25a8cb5935de24fc3d0e60a61032f6dc1d69388fb7fdac79d48279775d0",
    "templateCredsSetupCompleted": true
  },
  "name": "Newsletter to Audio Template v3",
  "tags": [],
  "nodes": [
    {
      "id": "efdedc12-d60f-441d-ba3a-5e331f3bb776",
      "name": "Note adhésive",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -640,
        -96
      ],
      "parameters": {
        "width": 432,
        "height": 1120,
        "content": "## 🎧 Newsletter-to-Audio Conversation Flow\n\nThis workflow turns unread newsletters from your email inbox into dynamic audio conversations between two AI voices — inspired by **Google's NotebookLM** ability to summarize and humanize dense content.\n\n### ✨ How it works:\n1. 📨 **Fetch Unread Newsletters** using Gmail (e.g., sender: \"your_favorite@newsletter.com\").\n2. 🧠 **Summarize and Reformat** the content into a dialogue using an LLM (like OpenAI or Gemini).\n3. 🗣️ **Generate Voices** for each part of the conversation using a TTS service (e.g., ElevenLabs, Google TTS).\n4. 🎛️ **Merge Audio Segments** into a natural back-and-forth flow using FFmpeg or audio nodes.\n5. 📤 **Send the Final Audio** file back to your email inbox (or deliver to another channel like Telegram or Drive).\n\n### 💡 Inspired by:\nNotebookLM's approach to making long-form text more digestible by turning it into personalized, conversational summaries.\n\nYou can modify:\n- 🎙️ The tone and voice of the personas\n- 🕵️ Email filters (e.g., subject or sender)\n- 📫 The delivery method (email, storage, etc.)\n\n> Ideal for turning passive subscriptions into engaging, hands-free content.\n\n---\n\n### 📬 Need help or want to collaborate?\n\nIf you have any questions, need help setting this up, or want to share feedback — feel free to reach out:  \n📩 **Luis.acosta@news2podcast.com**\n\nIf you're looking to build something more advanced with audio and AI — such as automatically updating podcasts to Spotify or other audio platforms — let me know and I’ll figure out how I can help you!\n"
      },
      "typeVersion": 1
    },
    {
      "id": "0d7b28ee-2163-4e5f-b552-1e85174a1a00",
      "name": "Obtenir le bulletin",
      "type": "n8n-nodes-base.gmailTrigger",
      "position": [
        0,
        0
      ],
      "parameters": {
        "simple": false,
        "filters": {
          "q": "from:demandcurve.com"
        },
        "options": {},
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        }
      },
      "credentials": {
        "gmailOAuth2": {
          "id": "kk1pIRVZu8RMHadC",
          "name": "Gmail account"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "37149829-5c91-4654-9173-e7c2617f0db3",
      "name": "Note adhésive1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -160,
        -96
      ],
      "parameters": {
        "color": 7,
        "width": 432,
        "height": 1104,
        "content": "## 📨 Step 1: Get Newsletter Content (Gmail or Webhook)\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nThis is the entry point of the workflow.\n\nBy default, we use the **Gmail node** to fetch unread newsletter emails from your inbox — perfect for personal automation. You can filter by sender with search \"from:\" (like Substack, Medium, or Beehiiv) to capture only relevant messages.\n\n### 🔁 Alternative: Webhook for Product Integration\n\nIf you're integrating this workflow into a larger product or service, you can **swap this Gmail node for a Webhook node** to receive newsletter or any other content via API from your app or users.\n\nThis makes it easy to embed the experience in platforms where users submit newsletter URLs, HTML, or pasted content manually.\n\n### ✅ Expected Output\n\n- `Body (HTML or plain text)`: Main content to be summarized and converted into a dialogue.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "54688146-cc4f-4499-9596-9fa11dac491e",
      "name": "Note adhésive2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        320,
        -96
      ],
      "parameters": {
        "color": 7,
        "width": 496,
        "height": 1104,
        "content": "## 🧠 Step 2: Generate Dialogue Script\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nThis node uses the **OpenAI Chat model (GPT-4o Mini)** to convert newsletter content into a natural, spoken-style conversation between two AI personas: `men1` and `men2`.\n\n### 🎯 Purpose\nTurn dense, static newsletter content into an engaging, human-like dialogue — similar to how **NotebookLM** rephrases documents as friendly, flowing discussions.\n\n### 🧑‍🤝‍🧑 Voice Personalities\n- `voice1`: Curious, expressive, informal — brings humor, reacts with emotion, and keeps it conversational.\n- `voice2`: Calm, reflective, slightly ironic — adds context, simplifies, and balances the tone.\n\n### 🧩 Prompt Logic\nThe custom prompt:\n- Sets a clear structure: intro, content breakdown, and closing\n- Encourages a spoken, spontaneous tone (not read-from-a-script)\n- Uses `<break time=\"1.5s\" />` tags to simulate realistic pauses\n- Demands **at least 10,000 characters** (~10 minutes of audio)\n- Output is formatted only as:\n   - voice1: …\n   - voice2: …\n\n### 📥 Input Source\n- Pulls the newsletter content from the Gmail node:\n`{{$('Get Newsletter').first().json.text}}`\n\n### ✅ Model Choice: GPT-4o Mini\nWe use **GPT-4o Mini** for its **excellent cost/performance ratio**, enabling fast generation of long-form dialogue at scale.\n\n> This is the core creative step of the workflow — translating static information into an immersive audio script, ready for voice synthesis.\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "d1cf8808-1af6-46d1-88c7-40cacd361cc1",
      "name": "Générer le script de dialogue",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        416,
        0
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini",
          "cachedResultName": "GPT-4O-MINI"
        },
        "options": {},
        "messages": {
          "values": [
            {
              "content": "=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 — 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’s 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: …\nvoice2: …\n\nYou will be given a newsletter input under this key:\n\n{{$('Get Newsletter').first().json.text}}\n\nGenerate only the final dialogue script — no explanations, bullet points, or headings. Just the conversation in English.\n\n\n"
            }
          ]
        }
      },
      "credentials": {
        "openAiApi": {
          "id": "qRfKea43wEoiDJoP",
          "name": "OpenAi account"
        }
      },
      "typeVersion": 1.8
    },
    {
      "id": "3254c9ca-c0f3-4ca2-bfb0-da126df9970c",
      "name": "Diviser le script",
      "type": "n8n-nodes-base.code",
      "position": [
        1024,
        0
      ],
      "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.message.content || \"\";\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": "c0953943-c607-456f-aeab-09544e40ba60",
      "name": "Note adhésive3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        848,
        -96
      ],
      "parameters": {
        "color": 7,
        "width": 496,
        "height": 1104,
        "content": "## ✂️ Step 3: Split Script into Speaker Segments\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nThis Function node takes the full dialogue script generated by OpenAI and **splits it into individual segments** based on speaker turns (`men1:` and `men2:`).\n\n### 🔧 What it does:\n- Reads the script from the previous node (`message.content`)\n- Normalizes line breaks (`\\r\\n` → `\\n`) for consistency\n- Uses a regex to split the text while **retaining speaker labels**\n- Filters out empty results\n- Returns each intervention as a separate item with:\n  ```json\n  {\n    \"segment\": \"men1: …\" \n  }\n\n### 📌 Why this is important\n\nEach segment will later be passed to the TTS system as a **standalone voice generation request**, allowing different voices to be applied for `men1` and `men2`.\n\nThis step transforms a long script into a list of **atomic, voice-ready dialogue chunks**.\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "121a604c-2d64-4c02-9801-7ec18c78dbaf",
      "name": "Boucler sur les éléments",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        1488,
        0
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "d2ad4b04-d346-4f79-998b-c73b9008024a",
      "name": "Si",
      "type": "n8n-nodes-base.if",
      "position": [
        1920,
        96
      ],
      "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": "b7a6be7e-3d9d-4eb9-9e26-a9ed1e8a84f5",
      "name": "Note adhésive4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1392,
        -96
      ],
      "parameters": {
        "color": 7,
        "width": 1184,
        "height": 2000,
        "content": "## 🔁 Step 4–6: Loop Through Segments & Generate Voices with ElevenLabs\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\nThis part of the workflow loops through each dialogue segment and sends it to **ElevenLabs** to generate realistic voice audio based on the speaker (`men1` or `men2`).\n\n\n### 🙌 Support this Template\n\nIf you'd like to support my work and help me continue building free, high-quality templates for the n8n community, **you can use my affiliate link when signing up for ElevenLabs**.  \n👉 It **doesn’t cost you anything extra**, and helps keep this project alive.  \n**[Click here to support via ElevenLabs](https://try.elevenlabs.io/ds0cvdfiufax)**\n\n\n### 🔧 How it works\n\n1. **Function Node – Clean Segment**  \n   - Removes problematic characters like quotation marks and line breaks\n   - Stores the cleaned result as `cleanedText`\n\n2. **IF Node – Detect Speaker**  \n   - Checks if `cleanedText` starts with `\"voice1:\"`  \n   - Branches into `voice1` or `voice2` to assign the correct voice\n\n3. **Function Node – Prepare Text for TTS**  \n   - Strips the `\"voice1:\"` or `\"voice2:\"` label using regex  \n   - Outputs a clean `modifiedString` to be sent to ElevenLabs\n\n4. **HTTP Request Node – ElevenLabs TTS**  \n   - Uses a **custom auth header**:\n     ```json\n     {\n       \"headers\": {\n         \"xi-api-key\": \"YOUR_API_KEY_FOR_ELEVENLABS\"\n       }\n     }\n     ```\n   - Endpoint:  \n     ```\n     https://api.elevenlabs.io/v1/text-to-speech/YOUR_VOICE_ID\n     ```\n    Replace `YOUR_VOICE_ID` with the voice you want to use from your ElevenLabs dashboard. After logging in, go to Voices, find the voice that best fits your needs, and copy the Voice ID by clicking on the three dots [...]\n\n   - Sample JSON body:\n     ```json\n     {\n       \"text\": \"Your cleaned and formatted dialogue text here.\",\n       \"model_id\": \"eleven_multilingual_v2\",\n       \"voice_settings\": {\n         \"stability\": 0.4,\n         \"similarity_boost\": 0.75\n       }\n     }\n     ```\n\n   - You can have one node for each speaker (`voice1`, `voice2`) with a different **voice ID** in the URL.\n\n---\n\n### 🔁 Output\n- Each iteration sends one segment to ElevenLabs.\n- The result will be a **binary audio file** (MP3) for each speaker’s line.\n- You can later merge them using FFmpeg or the Audio Merge node.\n\n---\n\n> This section is key to giving your newsletter content an actual voice — literally! And with ElevenLabs' high-quality synthesis and GPT-4o Mini's conversational structure, the result is natural, polished audio narration.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "e9926fad-2ec7-429b-9cbc-e13e537fd2f9",
      "name": "Nœud de fonction – Nettoyer le segment",
      "type": "n8n-nodes-base.code",
      "position": [
        1744,
        96
      ],
      "parameters": {
        "jsCode": "const paragraph = $input.first().json.segment; \nif (!paragraph) {\n    throw new Error(\"No se encontró contenido de texto en el correo.\");\n}\n\nlet cleanedText = paragraph\n  .replace(/\"/g, \"\")\n  .replace(/“/g, \"\")\n  .replace(/”/g, \"\");\n\ncleanedText = cleanedText.replace(/\\n/g, \"\");\n\nconsole.log(\"Texto limpio sin comillas ni saltos de línea:\", cleanedText);\n\nreturn [{ json: { cleanedText } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "2cab07f6-fb5c-4242-a0e9-702256e8708d",
      "name": "Nœud de fonction – Préparer le texte pour TTS",
      "type": "n8n-nodes-base.code",
      "position": [
        2128,
        160
      ],
      "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(\"men1:\")) {\n    console.log(\"✅ 'voice1:' detectado en el texto original.\");\n} else {\n    console.log(\"❌ 'voice1:' NO encontrado en el texto original. ¡Revisar input!\");\n}\n\n\nconst modifiedString = cleanedText.replace(/\\bvoice1:\\s*/gi, \"\").trim();\n\n\nconsole.log(\"Texto modificado:\", JSON.stringify(modifiedString, null, 2));\n\n\nif (modifiedString.includes(\"voice1:\")) {\n    console.log(\"❌ 'voice1:' sigue presente en el texto modificado. ¡El regex debe ajustarse!\");\n} else {\n    console.log(\"✅ 'voice1:' eliminado correctamente.\");\n}\n\n\nreturn [\n    {\n        json: {\n            modifiedString\n        }\n    }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "8b891a0d-9a6a-4b16-a459-02cf8f153c0a",
      "name": "Nœud de fonction – Préparer le texte pour TTS1",
      "type": "n8n-nodes-base.code",
      "position": [
        2128,
        368
      ],
      "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(\"✅ 'voice2:' detectado en el texto original.\");\n} else {\n    console.log(\"❌ 'voice2:' NO encontrado en el texto original. ¡Revisar 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(\"❌ 'voice2:' sigue presente en el texto modificado. ¡El regex debe ajustarse!\");\n} else {\n    console.log(\"✅ 'voice2:' eliminado correctamente.\");\n}\n\nreturn [\n    {\n        json: {\n            modifiedString\n        }\n    }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "e12b7916-44b5-4ed8-8b99-1485e8ecf36d",
      "name": "Nœud de fonction – Préparer le texte pour TTS - Voix 1",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2384,
        160
      ],
      "parameters": {
        "url": "=https://api.elevenlabs.io/v1/text-to-speech/uYXf8XasLslADfZ2MB4u",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"text\": \"{{ $json.modifiedString }}\",\n  \"model_id\": \"eleven_multilingual_v2\",\n  \"voice_settings\": {\n    \"stability\": 0.5,\n    \"similarity_boost\": 0.75\n  }\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpCustomAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "credentials": {
        "httpCustomAuth": {
          "id": "zPsZAr8WingF0NsY",
          "name": "Elevenlabs"
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.2
    },
    {
      "id": "9894a491-669b-493e-bd29-00281fe79150",
      "name": "Nœud de fonction – Préparer le texte pour TTS - Voix 2",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2384,
        368
      ],
      "parameters": {
        "url": "=https://api.elevenlabs.io/v1/text-to-speech/UgBBYS2sOqTuMpoF3BR0",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"text\": \"{{ $json.modifiedString }}\",\n  \"model_id\": \"eleven_multilingual_v2\",\n  \"voice_settings\": {\n    \"stability\": 0.5,\n    \"similarity_boost\": 0.75\n  }\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpCustomAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "credentials": {
        "httpCustomAuth": {
          "id": "zPsZAr8WingF0NsY",
          "name": "Elevenlabs"
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.2
    },
    {
      "id": "d46fbc80-a9ea-439a-84fc-e665f63170f4",
      "name": "Enregistrer les extraits audio",
      "type": "n8n-nodes-base.readWriteFile",
      "position": [
        2384,
        -16
      ],
      "parameters": {
        "options": {},
        "fileName": "=/newsletter2podcast/tmp/audio_{{$itemIndex}}.mp3",
        "operation": "write"
      },
      "typeVersion": 1
    },
    {
      "id": "4b7569cb-94d1-4a4a-9a24-4f1223c47be3",
      "name": "Note adhésive5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2608,
        -96
      ],
      "parameters": {
        "color": 7,
        "width": 672,
        "height": 1792,
        "content": "## 🎼 Step 7–8: Prepare FFmpeg List and Merge Audio\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nThis section merges all the individual voice segments into a single MP3 using FFmpeg, following the standard concat workflow via a `.txt` list file.\n\n⚠️ **To use this step, you must have FFmpeg installed and run n8n in a local or self-hosted environment.** This will not work on n8n Cloud or any environment that does not allow executing system commands.\n\n\n### 🧾 Step 7 – Generate `concat_list.txt`\n\nA **Code node** performs the following:\n\n1. Loops through all items, assuming each item has:\n   - `fileName`\n   - `directory` (only for the first file)\n2. Builds a properly formatted FFmpeg list:\n   ```\n   file 'audio_1.mp3'\n   file 'audio_2.mp3'\n   file 'audio_3.mp3'\n   ```\n3. Converts the result to **Base64-encoded binary data** so it can be saved using the `Write Binary File` node.\n\n📝 The result is written to:\n\n```\n/newsletter2podcast/tmp/concat_list.txt\n```\n\nCode logic highlights:\n- First item uses full path: `${directory}/${fileName}`\n- Others use just `fileName`\n- Converts the list to a `Buffer` and then to base64 for n8n binary handling\n\n---\n\n### 🎬 Step 8 – Merge Audio with FFmpeg\n\nAn `Execute Command` node runs:\n\n```bash\nffmpeg -y -f concat -safe 0 -i /newsletter2podcast/tmp/concat_list.txt -c copy /newsletter2podcast/tmp/final_merged.mp3\n```\n\nThis generates the final, merged audio file using **lossless concatenation**:\n\n- `-y`: Overwrites existing output\n- `-f concat`: Uses FFmpeg’s concat demuxer\n- `-safe 0`: Allows absolute paths\n- `-c copy`: Copies streams without re-encoding\n\n🆕 Output location:\n\n```\n/newsletter2podcast/tmp/final_merged.mp3\n```\n\nDelete the audio chunks and the concat_list\n\n```bash\nfind /newsletter2podcast/tmp/ -type f ! -name \"final_merged.mp3\" -delete\n```\n---\n\n### 📌 Notes\n\n- Ensure all audio files are encoded consistently (same format, codec, bitrate)\n- If ElevenLabs was used, they’re already compatible\n- If sync issues occur, you can preprocess files with re-encoding (`-c:a libmp3lame`)\n- This process assumes FFmpeg is installed and accessible in your n8n environment\n\n---\n\n> ✅ This step finalizes your audio transformation — from dialogue chunks to a smooth, podcast-ready episode.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "244a5ef9-14bd-4005-bba9-c224fea02c65",
      "name": "Générer `concat_list.txt`",
      "type": "n8n-nodes-base.code",
      "position": [
        2704,
        -16
      ],
      "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, index) => {\n  let filePath;\n\n\n  // Use only fileName for the rest\n    filePath = item.json.fileName;\n\n\n  if (filePath) {\n    concatListText += `file '${filePath}'\\n`;\n  }\n});\n\n// Convert the text to a Buffer, then to Base64\nconst buffer = Buffer.from(concatListText, 'utf-8');\nconst base64Data = buffer.toString('base64');\n\n// Return a single item containing the binary data\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": "a53ecd13-10fb-4d5b-9149-f82447804f07",
      "name": "Enregistrer concat_list",
      "type": "n8n-nodes-base.readWriteFile",
      "position": [
        2912,
        -16
      ],
      "parameters": {
        "options": {},
        "fileName": "/newsletter2podcast/tmp/concat_list.txt",
        "operation": "write"
      },
      "typeVersion": 1
    },
    {
      "id": "1259c775-2f7e-48be-b71a-8ace50bcf378",
      "name": "Joindre les extraits audio et supprimer tous les fichiers",
      "type": "n8n-nodes-base.executeCommand",
      "position": [
        3120,
        -16
      ],
      "parameters": {
        "command": "ffmpeg -y -f concat -safe 0 -i /newsletter2podcast/tmp/concat_list.txt -c copy /newsletter2podcast/tmp/final_merged.mp3\n\nfind /newsletter2podcast/tmp/ -type f ! -name \"final_merged.mp3\" -delete\n"
      },
      "typeVersion": 1
    },
    {
      "id": "5d5a1452-74be-40b4-901a-bf746581651a",
      "name": "Lire final_merged",
      "type": "n8n-nodes-base.readWriteFile",
      "position": [
        3424,
        -16
      ],
      "parameters": {
        "options": {},
        "fileSelector": "/newsletter2podcast/tmp/final_merged.mp3"
      },
      "typeVersion": 1
    },
    {
      "id": "c1caeef4-199e-43ad-a4ed-e95bbb1b2385",
      "name": "Envoyer l'audio",
      "type": "n8n-nodes-base.gmail",
      "position": [
        3632,
        -16
      ],
      "webhookId": "a01c59d4-d3d5-4aab-90fd-e87978735d23",
      "parameters": {
        "sendTo": "={{$('Get Newsletter').first().json.to.text}}",
        "message": "=<h1>Hello! Here your newsletter in Audio Version </h1>\n",
        "options": {
          "attachmentsUi": {
            "attachmentsBinary": [
              {}
            ]
          }
        },
        "subject": "=[Audio Version] {{$('Get Newsletter').first().json.subject}}"
      },
      "credentials": {
        "gmailOAuth2": {
          "id": "kk1pIRVZu8RMHadC",
          "name": "Gmail account"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "b27f0c70-2c69-4338-aa9f-3a0fad785d2c",
      "name": "Note adhésive6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3312,
        -96
      ],
      "parameters": {
        "color": 7,
        "width": 560,
        "height": 1184,
        "content": "## ✉️ Step 9: Read Merged Audio & Send via Email\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nThis final section completes the workflow by:\n\n1. **Reading the final merged MP3** using a `Read Binary File` node\n2. **Sending it as an email attachment** with a `Gmail` node\n\n---\n\n### 📂 Read Binary File\n\n- Path:  \n/newsletter2podcast/tmp/final_merged.mp3\n\n---\n\n### 📤 Gmail Node – Send Audio\n\nUse the `Gmail` node to:\n\n- Set the **recipient email** dynamically or statically\n- Add a subject and message body \n- Attach the binary file:\n- `Binary Property`: `data`\n- Attachment filename: `newsletter_audio.mp3`\n\n---\n\n### 📝 Notes\n\n- You can personalize the subject with dynamic values (like the original newsletter subject).\n- Make sure Gmail authentication is correctly configured in your credentials.\n- You may add logic before this node to notify users via Telegram, Slack, or cloud storage.\n\n---\n\n> ✅ This final step completes the journey from unread newsletter to hands-free audio experience, right in your inbox.\n"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "61e1c74c-8506-4907-a433-56b50ff33151",
  "connections": {
    "d2ad4b04-d346-4f79-998b-c73b9008024a": {
      "main": [
        [
          {
            "node": "2cab07f6-fb5c-4242-a0e9-702256e8708d",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "8b891a0d-9a6a-4b16-a459-02cf8f153c0a",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "c1caeef4-199e-43ad-a4ed-e95bbb1b2385": {
      "main": [
        []
      ]
    },
    "3254c9ca-c0f3-4ca2-bfb0-da126df9970c": {
      "main": [
        [
          {
            "node": "121a604c-2d64-4c02-9801-7ec18c78dbaf",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "0d7b28ee-2163-4e5f-b552-1e85174a1a00": {
      "main": [
        [
          {
            "node": "d1cf8808-1af6-46d1-88c7-40cacd361cc1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "121a604c-2d64-4c02-9801-7ec18c78dbaf": {
      "main": [
        [
          {
            "node": "d46fbc80-a9ea-439a-84fc-e665f63170f4",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "e9926fad-2ec7-429b-9cbc-e13e537fd2f9",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "a53ecd13-10fb-4d5b-9149-f82447804f07": {
      "main": [
        [
          {
            "node": "1259c775-2f7e-48be-b71a-8ace50bcf378",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "d46fbc80-a9ea-439a-84fc-e665f63170f4": {
      "main": [
        [
          {
            "node": "244a5ef9-14bd-4005-bba9-c224fea02c65",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "5d5a1452-74be-40b4-901a-bf746581651a": {
      "main": [
        [
          {
            "node": "c1caeef4-199e-43ad-a4ed-e95bbb1b2385",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "d1cf8808-1af6-46d1-88c7-40cacd361cc1": {
      "main": [
        [
          {
            "node": "3254c9ca-c0f3-4ca2-bfb0-da126df9970c",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "244a5ef9-14bd-4005-bba9-c224fea02c65": {
      "main": [
        [
          {
            "node": "a53ecd13-10fb-4d5b-9149-f82447804f07",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "e9926fad-2ec7-429b-9cbc-e13e537fd2f9": {
      "main": [
        [
          {
            "node": "d2ad4b04-d346-4f79-998b-c73b9008024a",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2cab07f6-fb5c-4242-a0e9-702256e8708d": {
      "main": [
        [
          {
            "node": "e12b7916-44b5-4ed8-8b99-1485e8ecf36d",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1259c775-2f7e-48be-b71a-8ace50bcf378": {
      "main": [
        [
          {
            "node": "5d5a1452-74be-40b4-901a-bf746581651a",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "8b891a0d-9a6a-4b16-a459-02cf8f153c0a": {
      "main": [
        [
          {
            "node": "9894a491-669b-493e-bd29-00281fe79150",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "e12b7916-44b5-4ed8-8b99-1485e8ecf36d": {
      "main": [
        [
          {
            "node": "121a604c-2d64-4c02-9801-7ec18c78dbaf",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "9894a491-669b-493e-bd29-00281fe79150": {
      "main": [
        [
          {
            "node": "121a604c-2d64-4c02-9801-7ec18c78dbaf",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Foire aux questions

Comment utiliser ce workflow ?

Copiez le code de configuration JSON ci-dessus, créez un nouveau workflow dans votre instance n8n et sélectionnez "Importer depuis le JSON", collez la configuration et modifiez les paramètres d'authentification selon vos besoins.

Dans quelles scénarios ce workflow est-il adapté ?

Avancé - Création de contenu, IA Multimodale

Est-ce payant ?

Ce workflow est entièrement gratuit et peut être utilisé directement. Veuillez noter que les services tiers utilisés dans le workflow (comme l'API OpenAI) peuvent nécessiter un paiement de votre part.

Informations sur le workflow
Niveau de difficulté
Avancé
Nombre de nœuds23
Catégorie2
Types de nœuds10
Description de la difficulté

Adapté aux utilisateurs avancés, avec des workflows complexes contenant 16+ nœuds

Auteur
Luis Acosta

Luis Acosta

@podcast-tools

I'm passionate about people and technology, with a hands-on and creative approach. Lately, my main focus has shifted towards process automation using artificial intelligence and the creation of meaningful content. I enjoy exploring how emerging technologies can simplify workflows, enhance productivity, and open new creative possibilities.

Liens externes
Voir sur n8n.io

Partager ce workflow

Catégories

Catégories: 34