{
  "nodes": [
    {
      "id": "64ef094b-c447-4dc3-a3c3-8b0d777b5759",
      "name": "GET Form",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        -2080,
        624
      ],
      "parameters": {
        "options": {
          "path": "audio-transcription",
          "ignoreBots": false,
          "respondWithOptions": {
            "values": {
              "formSubmittedText": "Your file has been received; an email will be sent to you upon completion of transcription or in case of error."
            }
          }
        },
        "formTitle": "Audio Transcription",
        "formFields": {
          "values": [
            {
              "fieldType": "file",
              "fieldLabel": "file",
              "requiredField": true,
              "acceptFileTypes": ".mp3"
            },
            {
              "fieldType": "email",
              "fieldLabel": "email",
              "requiredField": true
            }
          ]
        },
        "formDescription": "Select an audio file to transcribe and an email address to receive the result."
      },
      "typeVersion": 2.3
    },
    {
      "id": "7787660b-343e-49a0-8177-c77e263002fe",
      "name": "Make 4MiB Chunks",
      "type": "n8n-nodes-base.code",
      "position": [
        -1632,
        624
      ],
      "parameters": {
        "jsCode": "// 4 MiB chunks (FileFlows UI)\nconst CHUNK_SIZE = $(\"Configuration\").first().json.chunk_size;\n\nconst item = items[0];\n\n// Get the file uploaded by the Form Trigger\n// (the field is named \"file\" \u2192 binary.file; fallback to the first binary found)\nconst bin =\n  item.binary?.file ||\n  (item.binary ? item.binary[Object.keys(item.binary)[0]] : null);\n\nif (!bin?.data) {\n  throw new Error('No file received. Make sure the field is named \"file\".');\n}\n\nconst override =\n  item.json?.fields?.fileNameOverride?.toString().trim() ||\n  item.json?.fileNameOverride?.toString().trim() ||\n  '';\n\nconst fileName = override || bin.fileName || 'upload.bin';\nconst mimeType = bin.mimeType || 'application/octet-stream';\n\nconst buf = Buffer.from(bin.data, 'base64');\nconst totalChunks = Math.max(1, Math.ceil(buf.length / CHUNK_SIZE));\n\nconst out = [];\nfor (let i = 0; i < totalChunks; i++) {\n  const start = i * CHUNK_SIZE;\n  const end = Math.min(start + CHUNK_SIZE, buf.length);\n  const slice = buf.subarray(start, end);\n\n  out.push({\n    json: {\n      fileName,\n      chunkNumber: i,       // 0-based like the UI\n      totalChunks,\n      size: slice.length\n    },\n    binary: {\n      chunk: {\n        data: slice.toString('base64'),\n        fileName,\n        mimeType\n      }\n    }\n  });\n}\n\nreturn out;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "fc76d2f5-8ea6-4d8e-bf64-7772d1735860",
      "name": "Upload Chunk",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -960,
        688
      ],
      "parameters": {
        "url": "={{ $('Configuration').item.json.fileflows_url }}/api/library-file/upload",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "contentType": "multipart-form-data",
        "bodyParameters": {
          "parameters": [
            {
              "name": "fileName",
              "value": "={{$json[\"fileName\"]}}"
            },
            {
              "name": "chunkNumber",
              "value": "={{$json[\"chunkNumber\"]}}"
            },
            {
              "name": "totalChunks",
              "value": "={{$json[\"totalChunks\"]}}"
            },
            {
              "name": "file",
              "parameterType": "formBinaryData",
              "inputDataFieldName": "chunk"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "d0277bd8-7c34-4220-9005-4f1aa00c2409",
      "name": "Result",
      "type": "n8n-nodes-base.noOp",
      "position": [
        -1184,
        384
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "646cd48f-d686-4d8a-a477-f892c2bfbf4d",
      "name": "If succeed",
      "type": "n8n-nodes-base.if",
      "position": [
        -736,
        384
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "7cb6c8ab-074b-4ccf-a1e1-cc0163c9090d",
              "operator": {
                "type": "string",
                "operation": "exists",
                "singleValue": true
              },
              "leftValue": "={{ $json.data }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "9c135f4b-a927-4ed0-97c0-7e634be06668",
      "name": "Split audio file",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -512,
        240
      ],
      "parameters": {
        "url": "={{ $('Configuration').item.json.fileflows_url }}/api/library-file/manually-add",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"FlowUid\": \"{{ $('Configuration').first().json.flowUid }}\",\n  \"Files\": [\n    \"{{ $json.data }}\"\n  ],\n  \"CustomVariables\": {\n    \"callbackUrl\": \"{{$execution.resumeUrl}}\"\n  }\n} ",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "accept",
              "value": "*/*"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "035b6116-13ae-4077-b74c-f1f0f6ee5909",
      "name": "Wait",
      "type": "n8n-nodes-base.wait",
      "position": [
        -288,
        240
      ],
      "parameters": {
        "resume": "webhook",
        "options": {},
        "httpMethod": "POST",
        "resumeUnit": "minutes",
        "resumeAmount": 30,
        "limitWaitTime": true
      },
      "typeVersion": 1.1
    },
    {
      "id": "fbfdf35a-f759-40c0-ace6-5b92ad89dc51",
      "name": "Split Audio",
      "type": "n8n-nodes-base.code",
      "position": [
        -64,
        240
      ],
      "parameters": {
        "jsCode": "const binaries = $input.first().binary;\nconst entries = Object.entries(binaries);\n\nconst Audio = [];\nfor (let index = 0; index < entries.length; index++) {\n  const [key, value] = entries[index];\n\n  Audio.push({\n    json: {\n      fileExtension: value.fileExtension,\n      fileName: value.fileName,\n      fileSize: value.fileSize,\n      fileType: value.fileType,\n      mimeType: value.mimeType,\n    },\n    binary: {\n      [\"Audio\"]: value, // Preserve the complete binary structure\n    },\n  });\n}\n\n// \u26a0\ufe0f Important : send back array of items\nreturn Audio;"
      },
      "typeVersion": 2
    },
    {
      "id": "138f793f-72eb-4d6b-a91e-56d26986a4ec",
      "name": "Loop Over Segments",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        160,
        240
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "2bfd6a91-ba2d-4be5-b728-5f0fb4138fea",
      "name": "OpenAI",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "onError": "continueErrorOutput",
      "position": [
        608,
        336
      ],
      "parameters": {
        "options": {
          "language": "fr"
        },
        "resource": "audio",
        "operation": "transcribe",
        "binaryPropertyName": "Audio"
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 1.8,
      "waitBetweenTries": 5000
    },
    {
      "id": "fb77ddde-81bb-4d33-ad6c-18feb4554337",
      "name": "Result transcription",
      "type": "n8n-nodes-base.noOp",
      "position": [
        384,
        144
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "4e2d9d43-db6e-400e-b7c3-e6b9d30c28bd",
      "name": "Loop Over Chunks",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        -1408,
        624
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "3bdb454b-e54e-41da-a98d-5cf2c4049830",
      "name": "Merge transcription",
      "type": "n8n-nodes-base.code",
      "position": [
        608,
        144
      ],
      "parameters": {
        "jsCode": "let res = \"\";\n// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n  res += item.json.text;\n}\n\nreturn {\n  transcription: res\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "587d7489-863d-459b-bbc6-c902f0745bae",
      "name": "Convert to File",
      "type": "n8n-nodes-base.convertToFile",
      "position": [
        832,
        144
      ],
      "parameters": {
        "options": {
          "encoding": "utf8",
          "fileName": "transcription.txt"
        },
        "operation": "toText",
        "sourceProperty": "transcription"
      },
      "typeVersion": 1.1
    },
    {
      "id": "ce3854cc-d870-4a47-b931-676bdf3b2181",
      "name": "Configuration",
      "type": "n8n-nodes-base.set",
      "position": [
        -1856,
        624
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "cfb1a46f-20fa-4a40-b52e-da8249cbd31b",
              "name": "chunk_size",
              "type": "number",
              "value": "={{ 4 * 1024 * 1024 }}"
            },
            {
              "id": "eff0f114-6e49-43ef-aae0-a078dd671833",
              "name": "fileflows_url",
              "type": "string",
              "value": "http://0.0.0.0:5000"
            },
            {
              "id": "dbbed56f-1ca8-4106-aed8-14f7d6009068",
              "name": "flowUid",
              "type": "string",
              "value": "65fad732-0ac3-4f6d-ac10-2d31ef84c154"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "290bec95-3807-4d7f-abbc-affdeb54553e",
      "name": "Filter temporary files",
      "type": "n8n-nodes-base.filter",
      "position": [
        -960,
        384
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "46105005-e7f3-4fd9-9d2d-491a4bab372e",
              "operator": {
                "type": "string",
                "operation": "notEndsWith"
              },
              "leftValue": "={{ $json.data }}",
              "rightValue": ".temp"
            }
          ]
        }
      },
      "typeVersion": 2.2,
      "alwaysOutputData": true
    },
    {
      "id": "114c1df2-99b8-49bc-86c5-3404d27f6718",
      "name": "Rate Limit Delay",
      "type": "n8n-nodes-base.wait",
      "position": [
        832,
        400
      ],
      "parameters": {},
      "typeVersion": 1.1
    },
    {
      "id": "cfebb196-a52b-4594-8725-51236e0e4e22",
      "name": "Send Email with Transcription",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1056,
        144
      ],
      "parameters": {
        "sendTo": "={{ $('GET Form').first().json.email }}",
        "message": "Hi,\n\nYour audio transcription is complete and attached to this email.\n\nBest regards",
        "options": {
          "attachmentsUi": {
            "attachmentsBinary": [
              {}
            ]
          }
        },
        "subject": "Your transcription is ready"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "9b1de36f-6b8f-4044-9fa2-6dd8c641a51b",
      "name": "Send Error",
      "type": "n8n-nodes-base.gmail",
      "position": [
        848,
        608
      ],
      "parameters": {
        "sendTo": "={{ $('GET Form').first().json.email }}",
        "message": "Hi,\n\nWe encountered an issue with the translation model.\nPlease retry in a moment.\n\nBest",
        "options": {},
        "subject": "Your transcription encountered an issue"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "e1e89a89-165d-4aa1-b15a-d5aff0d0fc4d",
      "name": "Send Error1",
      "type": "n8n-nodes-base.gmail",
      "position": [
        -512,
        480
      ],
      "parameters": {
        "sendTo": "={{ $('GET Form').first().json.email }}",
        "message": "Hi,\n\nWe encountered an issue to split your file.\nPlease retry in a moment.\n\nBest",
        "options": {},
        "subject": "Your transcription encountered an issue"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "55862df0-b4b5-4078-b90b-998c893710b9",
      "name": "Chunk",
      "type": "n8n-nodes-base.noOp",
      "position": [
        -1184,
        576
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "d911c170-5767-49e7-8959-53cf809a7f4d",
      "name": "Segment",
      "type": "n8n-nodes-base.noOp",
      "position": [
        384,
        336
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "2aee5e34-e10d-4149-9dfc-ea05a2d5b4a8",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2848,
        -96
      ],
      "parameters": {
        "width": 676,
        "height": 1128,
        "content": "## Long-Form Audio Transcription\n\n**Problem:** OpenAI Whisper API has a 25 MB file size limit (~20 minutes of audio)\n\n**Solution:** This workflow overcomes the limitation by:\n1. Splitting audio into 15-minute segments (FileFlows + FFmpeg)\n2. Transcribing each segment individually (OpenAI Whisper)\n3. Merging all transcriptions into one file\n4. Emailing the complete result\n\n**Workflow Stages:**\n- **Stage 1:** Upload & Chunk (green area below)\n- **Stage 2:** Audio Splitting (blue area)\n- **Stage 3:** Transcription (orange area)\n- **Stage 4:** Delivery (purple area)\n\n**Use Cases:** Meetings, conferences, podcasts, interviews (hours-long recordings)\n\n**Documentation:** https://github.com/JulienDelRio/My-Interesting-n8n-Workflows/tree/main/Full%20audio%20transcription%20with%20FileFlows%20and%20OpenAI\n\n**Workflow for FileFlows** can be found the workflow here : https://github.com/JulienDelRio/My-Interesting-n8n-Workflows/blob/main/Full%20audio%20transcription%20with%20FileFlows%20and%20OpenAI/FileFlows%20-%20Split%20audio%20for%20n8n.json\n\n\n### \u2699\ufe0f Configuration Required\n\n**Before activation, configure:**\n\n1. **FileFlows Connection:**\n   - Update FileFlows URL in Configuration node\n   - Set correct Flow UID from FileFlows\n   - Ensure FileFlows is accessible from n8n\n\n2. **OpenAI Credentials:**\n   - Add OpenAI API key in credentials\n   - Assign to OpenAI node\n\n3. **Gmail Credentials:**\n   - Setup Gmail OAuth2 credentials\n   - Assign to all email nodes\n\n4. **Language (Optional):**\n   - Default: French (fr)\n   - Change in OpenAI node parameters\n   - Or remove for auto-detection\n\n**FileFlows Setup:**\n- Import FileFlows workflow\n- Install FFmpeg\n- Configure /media/segments/ storage"
      },
      "typeVersion": 1
    },
    {
      "id": "d0b1f7d2-8819-4ec9-9d33-183051233741",
      "name": "Stage 4 Delivery",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        352,
        -32
      ],
      "parameters": {
        "color": 7,
        "width": 876,
        "height": 132,
        "content": "## STEP 4: Delivery\n\nPackage transcription and email to user"
      },
      "typeVersion": 1
    },
    {
      "id": "cc22e417-d574-4098-b4f1-50c4690f3bfe",
      "name": "Stage 3 Transcription",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        176,
        784
      ],
      "parameters": {
        "color": 7,
        "width": 800,
        "height": 152,
        "content": "## STEP 3: OpenAI Transcription\n\nTranscribe each 15-minute segment using OpenAI Whisper API"
      },
      "typeVersion": 1
    },
    {
      "id": "5441f26c-c5df-4464-b794-e45c3199ddb5",
      "name": "Stage 2 Splitting",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -624,
        64
      ],
      "parameters": {
        "color": 7,
        "width": 720,
        "height": 140,
        "content": "## STEP 2: Audio Splitting with FileFlows\n\nSplit audio into 15-minute segments to stay under OpenAI's 25 MB limit."
      },
      "typeVersion": 1
    },
    {
      "id": "a0c0ed38-3f9a-4237-9012-a24a03e05c3d",
      "name": "Stage 1 Upload",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2112,
        912
      ],
      "parameters": {
        "color": 7,
        "width": 1280,
        "height": 134,
        "content": "## \ud83d\udce4 STEP 1: Upload & Chunk to FileFlows\n\nUpload large audio files to FileFlows in manageable chunks"
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Wait": {
      "main": [
        [
          {
            "node": "Split Audio",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Chunk": {
      "main": [
        [
          {
            "node": "Upload Chunk",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI": {
      "main": [
        [
          {
            "node": "Rate Limit Delay",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Result": {
      "main": [
        [
          {
            "node": "Filter temporary files",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Segment": {
      "main": [
        [
          {
            "node": "OpenAI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GET Form": {
      "main": [
        [
          {
            "node": "Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If succeed": {
      "main": [
        [
          {
            "node": "Split audio file",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send Error1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Audio": {
      "main": [
        [
          {
            "node": "Loop Over Segments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload Chunk": {
      "main": [
        [
          {
            "node": "Loop Over Chunks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configuration": {
      "main": [
        [
          {
            "node": "Make 4MiB Chunks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert to File": {
      "main": [
        [
          {
            "node": "Send Email with Transcription",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Chunks": {
      "main": [
        [
          {
            "node": "Result",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Chunk",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Make 4MiB Chunks": {
      "main": [
        [
          {
            "node": "Loop Over Chunks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Rate Limit Delay": {
      "main": [
        [
          {
            "node": "Loop Over Segments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split audio file": {
      "main": [
        [
          {
            "node": "Wait",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Segments": {
      "main": [
        [
          {
            "node": "Result transcription",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Segment",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge transcription": {
      "main": [
        [
          {
            "node": "Convert to File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Result transcription": {
      "main": [
        [
          {
            "node": "Merge transcription",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter temporary files": {
      "main": [
        [
          {
            "node": "If succeed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}