{
  "id": "v631KAaxCoXPZYnO",
  "name": "[TEMPLATE] AI Viral Script Generator + Whisper SRT",
  "tags": [],
  "nodes": [
    {
      "id": "webhook-1",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "onError": "continueRegularOutput",
      "position": [
        240,
        400
      ],
      "parameters": {
        "path": "viral-script-srt",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "respond-1",
      "name": "Respond",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1664,
        400
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={{ $json }}"
      },
      "typeVersion": 1.5
    },
    {
      "id": "gpt-1",
      "name": "Generate Script",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        464,
        400
      ],
      "parameters": {
        "url": "https://api.openai.com/v1/chat/completions",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ model: 'gpt-4o-mini', temperature: 0.8, messages: [{ role: 'system', content: 'You write punchline-first viral YouTube Shorts scripts. Structure: CLAIM (contrarian punchline, no setup, no \"did you know\") -> PROOF (specific verifiable fact with numbers or named studies) -> ESCALATION (stack 1-2 more facts). Cut hard on the strongest fact. No closer, no rhetorical question, no \"so next time...\". Plain ASCII only, no emojis, no markdown. Output ONLY the script text - no labels, no preamble.' }, { role: 'user', content: 'Write a ' + (($('Webhook').first().json.body.words) || 80) + '-word viral Short script on this topic: ' + (($('Webhook').first().json.body.topic) || 'a surprising fact about the human body') }] }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "openAiApi"
      },
      "typeVersion": 4.4
    },
    {
      "id": "extract-1",
      "name": "Extract Script",
      "type": "n8n-nodes-base.code",
      "position": [
        688,
        400
      ],
      "parameters": {
        "jsCode": "const raw = $input.first().json.choices[0].message.content || '';\nconst script = raw.trim();\nif (!script) throw new Error('GPT returned empty script');\nreturn [{ json: { script } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "tts-1",
      "name": "Generate TTS Audio",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        912,
        400
      ],
      "parameters": {
        "url": "https://api.openai.com/v1/audio/speech",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        },
        "jsonBody": "={{ JSON.stringify({ model: 'tts-1', input: $('Extract Script').first().json.script, voice: ($('Webhook').first().json.body.voice) || 'alloy', response_format: 'mp3' }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "openAiApi"
      },
      "typeVersion": 4.4
    },
    {
      "id": "whisper-1",
      "name": "Transcribe Whisper",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1120,
        400
      ],
      "parameters": {
        "url": "https://api.openai.com/v1/audio/transcriptions",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "contentType": "multipart-form-data",
        "authentication": "predefinedCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "model",
              "value": "whisper-1"
            },
            {
              "name": "response_format",
              "value": "verbose_json"
            },
            {
              "name": "timestamp_granularities[]",
              "value": "word"
            },
            {
              "name": "file",
              "parameterType": "formBinaryData",
              "inputDataFieldName": "data"
            }
          ]
        },
        "nodeCredentialType": "openAiApi"
      },
      "typeVersion": 4.4
    },
    {
      "id": "srt-1",
      "name": "Build SRT",
      "type": "n8n-nodes-base.code",
      "position": [
        1344,
        400
      ],
      "parameters": {
        "jsCode": "const item = $input.first();\nconst words = (item.json && item.json.words) || [];\nconst script = $('Extract Script').first().json.script;\nconst audioBase64 = item.binary && item.binary.data ? item.binary.data.data : '';\n\nfunction toSrtTime(s) {\n  const h = Math.floor(s / 3600);\n  const m = Math.floor((s % 3600) / 60);\n  const sec = Math.floor(s % 60);\n  const ms = Math.round((s - Math.floor(s)) * 1000);\n  return String(h).padStart(2, '0') + ':' + String(m).padStart(2, '0') + ':' + String(sec).padStart(2, '0') + ',' + String(ms).padStart(3, '0');\n}\n\nconst HARD_MAX = 5;\nconst IDEAL = 3;\nconst TAIL_HOLD = 0.5;\nconst breaks = /[.!?,;:]$/;\n\nconst cues = [];\nlet buf = [];\nfor (let i = 0; i < words.length; i++) {\n  const w = words[i];\n  buf.push(w);\n  const isLast = i === words.length - 1;\n  const endsBreak = breaks.test(w.word);\n  if (isLast || (buf.length >= 2 && endsBreak) || buf.length >= HARD_MAX) {\n    cues.push({ start: buf[0].start, end: buf[buf.length - 1].end, text: buf.map(b => b.word).join(' ').trim() });\n    buf = [];\n  } else if (buf.length >= IDEAL && words[i + 1] && /^[A-Z]/.test(words[i + 1].word)) {\n    cues.push({ start: buf[0].start, end: buf[buf.length - 1].end, text: buf.map(b => b.word).join(' ').trim() });\n    buf = [];\n  }\n}\n\nlet srt = '';\nfor (let i = 0; i < cues.length; i++) {\n  const c = cues[i];\n  const next = cues[i + 1];\n  const holdEnd = next ? next.start : (c.end + TAIL_HOLD);\n  srt += (i + 1) + '\\n' + toSrtTime(c.start) + ' --> ' + toSrtTime(holdEnd) + '\\n' + c.text + '\\n\\n';\n}\n\nreturn [{ json: { script, srt, audioBase64, durationSeconds: words.length ? words[words.length - 1].end : 0 } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "sticky-intro",
      "name": "Sticky Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -688,
        -160
      ],
      "parameters": {
        "width": 760,
        "height": 800,
        "content": "## AI Viral Script Generator + Whisper SRT\n\nOne webhook call returns everything you need to caption a YouTube Short: a punchline-first script, a narration MP3 (base64), and a frame-accurate SRT aligned to YOUR audio via Whisper word-level timestamps (perfect lip-sync). Drop the SRT into any editor, Shotstack, FFmpeg, etc.\n\n**In** (POST JSON): topic (required), words (default 80, ~30s), voice (alloy/echo/fable/onyx/nova/shimmer). **Out**: { script, srt, audioBase64, durationSeconds }.\n\n### How it works\n\n1. Webhook receives topic, word count and voice.\n2. GPT-4o-mini writes a punchline-first script (CLAIM -> PROOF -> ESCALATION, no closer).\n3. OpenAI TTS turns it into a narration MP3.\n4. Whisper word-timestamps the audio; Build SRT chunks it into TikTok-style cues.\n\n### Setup steps\n\n- [ ] Create an OpenAI credential (key at platform.openai.com/api-keys).\n- [ ] Select it in all 3 OpenAI nodes: Generate Script, Generate TTS Audio, Transcribe Whisper.\n\n### Try it\n\nClick Listen for test event on the Webhook, then POST:\n```json\n{\n  \"topic\": \"Why octopuses have three hearts\",\n  \"words\": 80,\n  \"voice\": \"nova\"\n}\n```\nReturns script + SRT in ~10s. Decode audioBase64 to a .mp3 file.\n\n### Customization\n\n~$0.012 per call (GPT-4o-mini + tts-1 + whisper-1). Tune the SRT chunking constants (HARD_MAX, IDEAL, TAIL_HOLD) at the top of Build SRT. Save the MP3 by adding a Write Binary File or Cloudinary node after Generate TTS Audio."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-setup",
      "name": "Sticky Script",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        432,
        128
      ],
      "parameters": {
        "color": 7,
        "width": 400,
        "height": 360,
        "content": "## Generate the script\n\nGPT-4o-mini writes a punchline-first viral Short script; Extract Script pulls the text and checks it is non-empty."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-test",
      "name": "Sticky Receive",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        160,
        128
      ],
      "parameters": {
        "color": 7,
        "width": 220,
        "height": 360,
        "content": "## Receive the request\n\nWebhook accepts a POST with topic, words and voice. See the overview note for a test payload."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-srt",
      "name": "Sticky Captions",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1088,
        128
      ],
      "parameters": {
        "color": 7,
        "width": 450,
        "height": 360,
        "content": "## Transcribe and build captions\n\nWhisper word-timestamps the audio; Build SRT chunks words into Shorts-style cues (sentence boundaries respected, no flicker)."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-audio",
      "name": "Sticky Audio",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        864,
        128
      ],
      "parameters": {
        "color": 7,
        "width": 180,
        "height": 360,
        "content": "## Create narration audio\n\ntts-1 outputs the MP3 as binary `data`, carried through to the response."
      },
      "typeVersion": 1
    },
    {
      "id": "1e20dbb7-70ee-4576-859e-548b8a16eaa1",
      "name": "Sticky Respond",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1552,
        128
      ],
      "parameters": {
        "color": 7,
        "width": 220,
        "height": 360,
        "content": "## Return the result\n\nResponds with { script, srt, audioBase64, durationSeconds }."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1",
    "saveManualExecutions": true,
    "saveExecutionProgress": true,
    "saveDataErrorExecution": "all",
    "saveDataSuccessExecution": "all"
  },
  "versionId": "2a822b5c-45c9-4773-99b2-29c64aad742c",
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Generate Script",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build SRT": {
      "main": [
        [
          {
            "node": "Respond",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Script": {
      "main": [
        [
          {
            "node": "Generate TTS Audio",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Script": {
      "main": [
        [
          {
            "node": "Extract Script",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate TTS Audio": {
      "main": [
        [
          {
            "node": "Transcribe Whisper",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Transcribe Whisper": {
      "main": [
        [
          {
            "node": "Build SRT",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}