{
  "id": "CkUYkY3qBRofQtX8",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "rippr \u2013 YouTube Transcript",
  "tags": [],
  "nodes": [
    {
      "id": "c6d7dbd1-2738-420b-b4d1-fe7801fa7372",
      "name": "Check Video Duration",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -192,
        -1696
      ],
      "parameters": {
        "url": "=https://youtube-v31.p.rapidapi.com/videos?part=contentDetails&id={{$('Extract YouTube video ID').item.json.videoId}}",
        "options": {},
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "x-rapidapi-host",
              "value": "youtube-v31.p.rapidapi.com"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "21b30c06-f013-4c90-8983-67990576d573",
      "name": "Receive Slack command",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -816,
        -1520
      ],
      "parameters": {
        "path": "rippr",
        "options": {
          "responseData": "\ud83d\udde1\ufe0f Ripping the full transcript\u2026 wait a few minutes, I\u2019ll post it here soon (\u2022 v \u2022)"
        },
        "httpMethod": "POST"
      },
      "typeVersion": 2.1
    },
    {
      "id": "13a3fbe3-f572-4b8c-8503-8e6dd6cfc968",
      "name": "Normalize Slack payload",
      "type": "n8n-nodes-base.set",
      "position": [
        -592,
        -1520
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "a2c85cf9-c94b-4eff-903b-0f3f331e951f",
              "name": "videoUrl",
              "type": "string",
              "value": "=Name: videoUrl\nValue: {{$json[\"body\"][\"text\"] || \"\"}}\nType: String"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "48ecd1bb-6b1d-4aff-806c-4c56b8048d40",
      "name": "Wait for transcription processing",
      "type": "n8n-nodes-base.wait",
      "position": [
        656,
        -1520
      ],
      "parameters": {
        "amount": "=20"
      },
      "typeVersion": 1.1
    },
    {
      "id": "b04a49e5-b9e0-440d-a1c3-187bfefacae6",
      "name": "Is transcription complete?",
      "type": "n8n-nodes-base.if",
      "position": [
        1248,
        -1520
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "b+1234567890ff-47fc-9460-d33980c7a4ee",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{$node[\" Check transcription status\"].json[\"status\"].toLowerCase()}}",
              "rightValue": "completed"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "dbe41b46-438d-4898-9bd1-73ff6942f756",
      "name": "Sticky Note16",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -800,
        -2640
      ],
      "parameters": {
        "width": 774,
        "height": 592,
        "content": "## How it works\n\nThis workflow lets users submit a YouTube link from Slack and receive the processed result directly in the same Slack channel.\n\nWhen a user triggers the Slack command, the workflow extracts the YouTube video ID and checks the video duration using the YouTube Data API. If the video exceeds the supported duration limit, the workflow stops early and sends a clear error message back to Slack.\n\nFor valid videos, the workflow converts the YouTube video into an audio file and submits it to AssemblyAI to start a transcription job. The workflow waits while the transcription is processed and periodically checks the transcription status until it is complete.\n\nOnce the transcript is ready, the workflow processes the transcript text and, if enabled, generates an AI summary. The final formatted result is then posted back to Slack using the original response URL.\n\nThis design minimizes unnecessary API usage and provides users with fast, readable results without leaving Slack.\n\n## Setup steps\n\n1. Create a Slack slash command and configure it to send requests to the Webhook node.\n2. Add API credentials for YouTube Data API, RapidAPI, AssemblyAI, and OpenAI.\n3. Adjust the maximum allowed video duration if needed.\n4. Activate the workflow and test it by submitting a YouTube link from Slack."
      },
      "typeVersion": 1
    },
    {
      "id": "b3b7f7cd-22db-42dd-b231-f2e4df87907f",
      "name": "Extract YouTube video ID",
      "type": "n8n-nodes-base.code",
      "position": [
        -368,
        -1696
      ],
      "parameters": {
        "jsCode": "/**\n * Extracts the YouTube URL and 11-char video ID from a Slack slash command payload.\n * Tailored for /rippit (removes the command prefix).\n */\n\n// Matches typical YouTube forms: watch?v=, youtu.be/, shorts/, embed/\nconst YT_RE = /(?:youtube\\.com\\/watch\\?v=|youtu\\.be\\/|shorts\\/|embed\\/)([A-Za-z0-9_-]{11})/i;\n\nfunction toItem(inputJson = {}) {\n  // Slack sends the text in body.text; fall back to text for test runs\n  const raw = (inputJson?.body?.text ?? inputJson?.text ?? '').toString();\n\n  // Remove the \"/rippit\" command prefix (and any extra spaces)\n  const text = raw.replace(/^\\/rippit\\s*/, '').trim();\n\n  // Try to grab the first URL if user pasted text + link; otherwise treat text as URL\n  const urlMatch = text.match(/https?:\\/\\/\\S+/);\n  const url = urlMatch ? urlMatch[0] : text;\n\n  // Extract the canonical 11-char video ID (null if not matched)\n  const idMatch = url.match(YT_RE);\n  const videoId = idMatch ? idMatch[1] : null;\n\n  return { json: { text, videoUrl: url, videoId } };\n}\n\n// Support both single-item and aggregated runs\nconst itemsIn = $input.all();\nreturn itemsIn.length ? itemsIn.map(i => toItem(i.json)) : [toItem($json)];"
      },
      "typeVersion": 2
    },
    {
      "id": "44f54553-b267-40ae-9daf-0c25ac019abb",
      "name": "Parse and validate video duration",
      "type": "n8n-nodes-base.code",
      "position": [
        16,
        -1696
      ],
      "parameters": {
        "jsCode": "// Get the duration from the API response\nconst duration = $input.item.json.items[0].contentDetails.duration;\n\n// Parse ISO 8601 duration format (e.g., PT10M30S, PT1H5M, PT45S)\nconst match = duration.match(/PT(?:(\\d+)H)?(?:(\\d+)M)?(?:(\\d+)S)?/);\n\nconst hours = parseInt(match[1] || 0);\nconst minutes = parseInt(match[2] || 0);\nconst seconds = parseInt(match[3] || 0);\n\n// Calculate total minutes\nconst totalMinutes = hours * 60 + minutes + seconds / 60;\n\n// Get data from Extract YouTube node\nconst extractYouTubeNode = $('Extract YouTube video ID').first();\n\n// Check if video is too long\nif (totalMinutes > 10) {\n  // Return error flag instead of throwing\n  return {\n    json: {\n      error: true,\n      errorMessage: `Video is too long: ${Math.round(totalMinutes)} minutes. Maximum allowed is 10 minutes.`,\n      videoId: extractYouTubeNode.json.videoId,\n      videoUrl: extractYouTubeNode.json.videoUrl\n    }\n  };\n}\n\n// Pass through for valid videos\nreturn {\n  json: {\n    error: false,\n    videoId: extractYouTubeNode.json.videoId,\n    videoUrl: extractYouTubeNode.json.videoUrl,\n    durationMinutes: Math.round(totalMinutes * 10) / 10\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "93e43fda-6847-4856-9670-a37c895dea2c",
      "name": "Is video longer than limit?",
      "type": "n8n-nodes-base.if",
      "position": [
        224,
        -1696
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "0bbbcbbd-062a-45b6-959b-5eda8f99243f",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{$json.error}}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "e82d0b5b-4ab9-4473-9774-76b912fb85cd",
      "name": "Send duration error to Slack",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        448,
        -1840
      ],
      "parameters": {
        "url": "={{$('Receive Slack command').item.json.body.response_url}}",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "text",
              "value": "=\u274c {{$json.errorMessage}}"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "79dec261-0783-4c88-9b28-90bae3349381",
      "name": "Convert YouTube video to MP3",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        0,
        -1392
      ],
      "parameters": {
        "url": "={{'https://youtube-mp3-audio-video-downloader.p.rapidapi.com/download-mp3/' + $json.videoId + '?quality=low'}}",
        "options": {
          "response": {
            "response": {
              "neverError": true,
              "fullResponse": true,
              "responseFormat": "file",
              "outputPropertyName": "mp3"
            }
          }
        },
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "content-type ",
              "value": "application/json"
            },
            {
              "name": "x-rapidapi-host",
              "value": "youtube-mp3-audio-video-downloader.p.rapidapi.com"
            },
            {
              "name": "accept",
              "value": "application/octet-stream"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "c4b7332e-dc11-43bc-b712-831efdf7b421",
      "name": " Create transcription job",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        240,
        -1392
      ],
      "parameters": {
        "url": "=https://api.assemblyai.com/v2/upload",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "sendBody": true,
        "contentType": "binaryData",
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "content-type",
              "value": "application/octet-stream"
            }
          ]
        },
        "inputDataFieldName": "mp3"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "6e16539f-2dfb-4ca3-a20a-b892408de5a5",
      "name": "Submit audio for transcription",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        464,
        -1568
      ],
      "parameters": {
        "url": "https://api.assemblyai.com/v2/transcript",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"audio_url\": \"{{$json['upload_url']}}\"\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "146bb207-4b24-470c-9a74-2f1ad6bf42aa",
      "name": " Check transcription status",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        864,
        -1520
      ],
      "parameters": {
        "url": "=https://api.assemblyai.com/v2/transcript/{{$node[\"Submit audio for transcription\"].json[\"id\"]}}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "d4d851ba-0d13-4f07-b8d2-b6e6f8fdcc14",
      "name": "Extract transcript text",
      "type": "n8n-nodes-base.code",
      "position": [
        1072,
        -1520
      ],
      "parameters": {
        "jsCode": "// Read the mode that Edit Fields already decided\nconst mode = ($items('Normalize Slack payload', 0, 0)?.json?.mode) || 'summary';\n\n// Pass the transcript text from the AssemblyAI HTTP Request node ($json.text)\nreturn [{\n  json: {\n    transcript: $json.text ?? '',\n    mode\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "9aff81af-fc3e-4d4e-bd9f-bb3a4cb96015",
      "name": "Generate AI summary",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        1456,
        -1536
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4-turbo",
          "cachedResultName": "GPT-4-TURBO"
        },
        "options": {},
        "messages": {
          "values": [
            {
              "role": "system",
              "content": "=You are a text cleaner.\nReformat this transcript for readability.\nKeep all spoken content intact, but remove unnecessary line breaks, timestamps, and filler symbols.\nDo not summarize, just make it look like readable paragraphs.\nOutput only the cleaned text."
            },
            {
              "content": "={{$node[\"Extract transcript text\"].json[\"transcript\"]}}"
            }
          ]
        }
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.8
    },
    {
      "id": "cd448f13-f252-4b9c-83f4-0d2ca9f825d8",
      "name": "Post result to Slack",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1744,
        -1536
      ],
      "parameters": {
        "url": "={{$node[\"Receive Slack command\"].json[\"body\"][\"response_url\"]}}",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "sendHeaders": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "=response_type",
              "value": "in_channel"
            },
            {
              "name": "text",
              "value": "=)\u2019(0)\u2019(  RIPPIT  RIPPIT  )\u2019(0)\u2019<\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\nExtracted by rippr. \n\u00a9 All rights belong to the original video creator. \nrippr does not own or claim any content. This transcript is provided for informational purposes only.\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\n<{{$node[\"Extract YouTube video ID\"].json[\"videoUrl\"]}}|:film_projector: Original Video>\n\n{{$node[\"Generate AI summary\"].json[\"message\"][\"content\"] ?? ($json[\"message\"] ? $json[\"message\"][0][\"content\"] : \"\")}}"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": " Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "05ecbcb1-6917-47f3-b63d-65231f209878",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -816,
        -1664
      ],
      "parameters": {
        "color": 7,
        "height": 96,
        "content": "Receives the Slack command and normalizes the incoming payload."
      },
      "typeVersion": 1
    },
    {
      "id": "acfccd91-a264-4bf1-a2a3-be1cdd44d805",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -368,
        -1856
      ],
      "parameters": {
        "color": 7,
        "height": 112,
        "content": "Extracts the YouTube video ID and validates video duration before processing."
      },
      "typeVersion": 1
    },
    {
      "id": "1b4308b6-8bd5-4569-8e58-eb02e5d0accf",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -16,
        -1200
      ],
      "parameters": {
        "color": 7,
        "height": 96,
        "content": "Converts the video to audio and starts the transcription job."
      },
      "typeVersion": 1
    },
    {
      "id": "6891f2bb-8fb0-48ae-8817-97f15037e412",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        656,
        -1680
      ],
      "parameters": {
        "color": 7,
        "height": 112,
        "content": "Waits for transcription completion and retrieves the final transcript."
      },
      "typeVersion": 1
    },
    {
      "id": "6db11ea7-d303-4f30-b976-9faa2e4e4d16",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1744,
        -1664
      ],
      "parameters": {
        "color": 7,
        "height": 96,
        "content": "Formats the result and sends it back to Slack."
      },
      "typeVersion": 1
    }
  ],
  "active": true,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "4f8147a1-4412-4ebf-9831-b44c0d314384",
  "connections": {
    "Generate AI summary": {
      "main": [
        [
          {
            "node": "Post result to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Video Duration": {
      "main": [
        [
          {
            "node": "Parse and validate video duration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Receive Slack command": {
      "main": [
        [
          {
            "node": "Normalize Slack payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract transcript text": {
      "main": [
        [
          {
            "node": "Is transcription complete?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Slack payload": {
      "main": [
        [
          {
            "node": "Extract YouTube video ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract YouTube video ID": {
      "main": [
        [
          {
            "node": "Check Video Duration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    " Create transcription job": {
      "main": [
        [
          {
            "node": "Submit audio for transcription",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is transcription complete?": {
      "main": [
        [
          {
            "node": "Generate AI summary",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wait for transcription processing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    " Check transcription status": {
      "main": [
        [
          {
            "node": "Extract transcript text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is video longer than limit?": {
      "main": [
        [
          {
            "node": "Send duration error to Slack",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Convert YouTube video to MP3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert YouTube video to MP3": {
      "main": [
        [
          {
            "node": " Create transcription job",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Submit audio for transcription": {
      "main": [
        [
          {
            "node": "Wait for transcription processing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse and validate video duration": {
      "main": [
        [
          {
            "node": "Is video longer than limit?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait for transcription processing": {
      "main": [
        [
          {
            "node": " Check transcription status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}