| name | reel-script-writer |
| description | Reverse-engineer a reference Instagram Reel and rewrite it as a fresh, ready-to-record Reel script on a topic you choose. Takes a public Instagram Reel URL, scrapes it via apify/instagram-scraper, runs the video through Gemini 2.5 Flash for a full transcript + hook + structure analysis, then writes a new script that applies the same proven patterns to your topic. Use whenever the user says "script a reel", "write me a reel", "turn this into a reel", or pastes an Instagram Reel URL. Requires APIFY_API_TOKEN and GOOGLE_AI_API_KEY.
|
Reel Script Writer
This skill does ONE thing well: you give a reference Reel and a topic, it writes the script.
CRITICAL: Auto-start on load
When this skill triggers, go straight to Step 1. Do not summarise the skill back to the user.
Prerequisites
- Node.js 18 or newer (the script uses the built-in
fetch).
- Two API keys set as environment variables:
APIFY_API_TOKEN — Instagram scraping (apify.com → Settings → API tokens)
GOOGLE_AI_API_KEY — Gemini 2.5 Flash video analysis (aistudio.google.com → Get API key)
Confirm both exist:
env | grep -E "APIFY_API_TOKEN|GOOGLE_AI_API_KEY"
If either is empty, tell the user to add it to their shell profile (~/.zprofile on macOS zsh, ~/.bashrc on bash), reload it, then stop. Do not continue without both keys.
- Optional voice file — placeholder
<YOUR_VOICE_NOTES_FILE>. If the user keeps a notes file describing their own tone/audience, read it before writing and match that voice. If there is no such file, use the built-in writing principles in Step 4.
Step 1. Get the reference Reel URL
Ask:
Paste a public Instagram Reel URL you want me to learn from (instagram.com/reel/... or instagram.com/p/...).
Validate it matches instagram.com/(reel|p|tv)/. If it does not, ask again. Never fabricate analysis from a URL you could not scrape.
Step 2. Get the topic
Ask:
What topic should this Reel cover? (a sentence or two is fine)
Wait for the topic. That is the only input the script needs.
Step 3. Set up the workspace, scrape, and analyse
- Create the working folder if it does not exist:
~/Desktop/Reels/
- Write the analyser to
~/Desktop/Reels/analyse-reel.js using the exact script in the code block below.
- Install dependencies if
~/Desktop/Reels/node_modules/ is missing:
cd ~/Desktop/Reels && npm install apify-client @google/generative-ai
- Scrape the Reel (downloads the video + raw metadata):
node ~/Desktop/Reels/analyse-reel.js "<reel_url>" scrape
It prints owner, view/like/comment counts, caption preview, and file size. Show those to the user.
- Analyse the video with Gemini 2.5 Flash:
node ~/Desktop/Reels/analyse-reel.js "<reel_url>" analyse
The analysis (full transcript, hook breakdown, on-screen overlays, language patterns, structure, key insight) is saved to ~/Desktop/Reels/analyses/. Read that file before writing.
Small videos are sent inline; videos over ~18 MB are auto-uploaded via the Gemini Files API (no extra dependency — it ships inside @google/generative-ai). If the scrape returns zero items across both attempts, report the failure and stop. Do not invent an analysis. Gemini 2.5 Flash is the analysis model — do not substitute it.
~/Desktop/Reels/analyse-reel.js (write this verbatim)
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { ApifyClient } = require('apify-client');
const { GoogleGenerativeAI } = require('@google/generative-ai');
const { GoogleAIFileManager } = require('@google/generative-ai/server');
const BASE = __dirname;
const DOWNLOADS = path.join(BASE, 'downloads');
const ANALYSES = path.join(BASE, 'analyses');
const INLINE_LIMIT = 18 * 1024 * 1024;
function shortCodeFromUrl(url) {
const m = url.match(/instagram\.com\/(?:reel|p|tv)\/([^/?#]+)/i);
return m ? m[1] : null;
}
function slugFor(item) {
const owner = (item.ownerUsername || 'unknown').replace(/[^a-z0-9_.-]/gi, '');
const code = item.shortCode || shortCodeFromUrl(item.url || '') || 'reel';
return `${owner}_${code}`;
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function scrapeItem(url) {
const client = new ApifyClient({ token: process.env.APIFY_API_TOKEN });
const variants = [
{ directUrls: [url], resultsType: 'posts', resultsLimit: 1, addParentData: false },
{ urls: [url] },
];
for (const input of variants) {
const run = await client.actor('apify/instagram-scraper').call(input);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
if (items && items.length) return items[0];
}
return null;
}
async function download(url, dest) {
const res = await fetch(url);
if (!res.ok) throw new Error(`download failed: HTTP ${res.status}`);
const buf = Buffer.from(await res.arrayBuffer());
fs.writeFileSync(dest, buf);
return buf.length;
}
async function doScrape(url) {
fs.mkdirSync(DOWNLOADS, { recursive: true });
const item = await scrapeItem(url);
if (!item) {
console.error('SCRAPE_FAILED: zero items returned. The Reel may be private or removed.');
process.exit(1);
}
const slug = slugFor(item);
fs.writeFileSync(path.join(DOWNLOADS, `${slug}.json`), JSON.stringify(item, null, 2));
if (!item.videoUrl) {
console.error('SCRAPE_FAILED: no videoUrl on the scraped item.');
process.exit(1);
}
const bytes = await download(item.videoUrl, path.join(DOWNLOADS, `${slug}.mp4`));
console.log(JSON.stringify({
ok: true,
mp4: path.join(DOWNLOADS, `${slug}.mp4`),
json: path.join(DOWNLOADS, `${slug}.json`),
sizeKB: Math.round(bytes / 1024),
owner: item.ownerUsername || null,
views: item.videoViewCount || item.videoPlayCount || null,
likes: item.likesCount || null,
comments: item.commentsCount || null,
caption: (item.caption || '').slice(0, 200),
}, null, 2));
}
function findMp4(code) {
if (!fs.existsSync(DOWNLOADS)) return null;
const f = fs.readdirSync(DOWNLOADS).find((n) => n.endsWith(`_${code}.mp4`));
return f ? path.join(DOWNLOADS, f) : null;
}
function analysisPrompt() {
return `I'm studying this Reel to write my own script in a similar style.
## Full Transcript
- Transcribe EVERY word with timestamps
## On-Screen Text Overlays
- Transcribe every text overlay that appears on screen, with start-end timestamps and on-screen position (top / middle / bottom, left / center / right)
- For each overlay, label its purpose: hook (mute-viewer scroll stop), keyword emphasis, evidence anchor, statistic highlight, CTA reinforcement, brand or handle, or transition
- Flag the OPENING overlay specifically. For mute viewers, that overlay IS the hook. List it under a clearly named "Opening overlay (mute-viewer hook)" subheading and quote the exact text
- Note styling cues that carry meaning: all caps, bold weight, color contrast, large size
- If overlays differ from the spoken hook, point out the gap and what each is doing differently
## Hook
- Exact first words spoken
- Word count of the hook
- What makes it stop the scroll?
- Compare the spoken hook to the opening on-screen overlay: which one is doing the heavy lifting?
## Language Patterns
- Average sentence length
- You/your vs I/me ratio
- Transitions between points
- Where are the minimisers (just, only, simply)?
## Structure
- Total duration
- Section breakdown with timings
- What's the before/after moment?
- What's the CTA?
## One key insight
- The single most important technique to learn from this Reel`;
}
async function uploadViaFilesApi(mp4) {
const fileManager = new GoogleAIFileManager(process.env.GOOGLE_AI_API_KEY);
const upload = await fileManager.uploadFile(mp4, {
mimeType: 'video/mp4',
displayName: path.basename(mp4),
});
let file = await fileManager.getFile(upload.file.name);
while (file.state === 'PROCESSING') {
await sleep(2000);
file = await fileManager.getFile(upload.file.name);
}
if (file.state === 'FAILED') throw new Error('Gemini Files API: video processing failed.');
return { fileData: { mimeType: file.mimeType, fileUri: file.uri } };
}
async function doAnalyse(url) {
fs.mkdirSync(ANALYSES, { recursive: true });
const code = shortCodeFromUrl(url);
const mp4 = code && findMp4(code);
if (!mp4) {
console.error('ANALYSE_FAILED: no downloaded video found. Run the "scrape" step first.');
process.exit(1);
}
const size = fs.statSync(mp4).size;
let videoPart;
if (size > INLINE_LIMIT) {
videoPart = await uploadViaFilesApi(mp4);
} else {
const b64 = fs.readFileSync(mp4).toString('base64');
videoPart = { inlineData: { mimeType: 'video/mp4', data: b64 } };
}
const genAI = new GoogleGenerativeAI(process.env.GOOGLE_AI_API_KEY);
const model = genAI.getGenerativeModel({ model: 'gemini-2.5-flash' });
const result = await model.generateContent([videoPart, { text: analysisPrompt() }]);
const text = result.response.text();
const out = path.join(ANALYSES, path.basename(mp4).replace(/\.mp4$/, '.md'));
fs.writeFileSync(out, text);
console.log(JSON.stringify({
ok: true,
analysis: out,
chars: text.length,
transport: size > INLINE_LIMIT ? 'files-api' : 'inline',
}, null, 2));
}
(async () => {
const [, , url, mode] = process.argv;
if (!url || !mode) {
console.error('Usage: node analyse-reel.js "<reel_url>" scrape | analyse');
process.exit(1);
}
if (mode === 'scrape') {
if (!process.env.APIFY_API_TOKEN) { console.error('Missing APIFY_API_TOKEN'); process.exit(1); }
await doScrape(url);
} else if (mode === 'analyse') {
if (!process.env.GOOGLE_AI_API_KEY) { console.error('Missing GOOGLE_AI_API_KEY'); process.exit(1); }
await doAnalyse(url);
} else {
console.error('Unknown mode:', mode, '(use "scrape" or "analyse")');
process.exit(1);
}
})().catch((e) => { console.error('ERROR:', e.message); process.exit(1); });
Step 4. Write the new Reel script
Using the Gemini analysis, the topic from Step 2, and (if present) <YOUR_VOICE_NOTES_FILE>, write a fresh Reel script in the artist's own voice, talking to their fans. Mirror the reference's structure — hook word count, beat timing, CTA shape — never its words.
Output path: ~/Desktop/Reels/scripts/{YYYY-MM-DD}-{slug}.md (create the scripts folder if needed).
Where to keep your scripts: ~/Desktop/Reels/scripts/ is just the default. For a searchable, linkable library that survives long-term, store your output in a note-taking tool like Obsidian — point the output path at a folder inside your vault (e.g. ~/Obsidian/<YOUR_VAULT>/Reels/). It is your call where it lives; the skill only needs a writable folder path.
Writing principles (self-contained — no external files)
Hook — every hook, spoken AND on-screen, must do all three:
- Grab attention — open with a big idea that triggers emotion (fear, loss, surprise, desire) or promises a clear benefit. A flat statement is not a hook.
- Open a loop — create curiosity within 3 seconds. Never give the question and the answer together.
- Speak to your listeners — write as if you're talking straight to your fans, so the right person feels "this is for me". Name the thing they care about (your next release, your sound, the show). Never skip this.
- Never open with "I". Use "this", "you", a fact, a name, or a number.
- Proven formats: "This changed [X] forever" / negative flip ("[X] is useless unless...") / capability statement / specific number.
- Match the hook's word count to the reference.
On-screen hook options: generate 3-5 options, each hitting all three steps above. At least one must carry a specific, honest number. Present them ranked, pick the strongest honest one. Never invent a number to look bigger.
Body:
- US English. Short sentences. Conversational "you".
- Never state the conclusion outright — let the facts land it.
- No "link in bio". Drive to a comment trigger instead.
- Banned (do not use): em dashes, semicolons, emojis, "unlock", "supercharge", "powerful", "game-changer", "revolutionary", "nobody talks about", "most people don't realize".
Comment trigger CTA: a single ALL-CAPS word (e.g. SCRIPT, GUIDE, STACK) directly tied to what's promised. No quotes, no "below", no trailing punctuation. Format: Comment [WORD] and I'll send you [specific thing].
Duration & structure: 30-45 seconds. 2 key points maximum. The caption mirrors the script.
On-screen overlays:
- Opening overlay (0-3s): ALL CAPS, top of frame, 4-8 words, holds the full hook. This is the scroll-stop for muted viewers. It may echo the spoken hook (verbatim or trimmed).
- One overlay per beat (Point 1, Point 2, CTA): 3-7 words. Body overlays must distill (a number, tool name, or stake) — never repeat the spoken sentence verbatim. The opening overlay is the only exception.
- Overlays obey the same banned list.
Script file template
---
type: reel-script
date: [YYYY-MM-DD]
slug: [kebab-case-slug]
title: [title]
status: Draft
duration_target: 30-45s
comment_trigger: [WORD]
deliverable: [what gets DM'd]
reference_url: [reel url]
reference_owner: [@username]
reference_views: [integer or n/a]
reference_analysis_file: ~/Desktop/Reels/analyses/[owner]_[shortcode].md
---
# Reel: [title]
## Reference (link only — no transcript pasted here)
- URL: [reel url]
- Owner: [@username]
- Views / Likes / Comments: [from scrape]
- Full analysis: ~/Desktop/Reels/analyses/[owner]_[shortcode].md
## On-screen hook options (ranked)
1. [option] — [why it works]
2. ...
## Hook (0-3s)
[exact words]
## Point 1 ([start]-[end]s)
[exact words]
## Point 2 ([start]-[end]s)
[exact words]
## CTA ([start]-[end]s)
Comment [WORD] and I'll send you [specific deliverable].
---
## Caption
[mirrors the script, formatted for Instagram]
## On-Screen Text Overlays
### Opening overlay (0-3s) — mute-viewer hook
- Position: top, center
- Style: all caps, bold
- Text: [4-8 words]
### Point 1 overlay ([start]-[end]s)
- Position: [...]
- Style: [stat / keyword chip]
- Text: [3-7 words]
### Point 2 overlay ([start]-[end]s)
- Position: [...]
- Style: [...]
- Text: [3-7 words]
### CTA overlay ([start]-[end]s)
- Position: bottom, center
- Style: button-style chip
- Text: Comment [WORD]
## Visual notes
[cuts, B-roll ideas, sequencing]
Step 5. QA — score before showing
Score the draft against every principle in Step 4. Fix every violation and re-score until it clears 95/100. Never show a draft below 95.
Check for: opens with "I"; banned phrases; em dashes or semicolons; missing or weak open loop; a hook that doesn't speak to the listener; multi-word or lowercase comment trigger; more than 2 points; over 45 seconds when read aloud (~140 wpm); caption not mirroring the script; a body overlay repeating its spoken sentence verbatim; any invented number.
Report one line before the script: QA: 96/100 — ready.
Step 6. Deliver
Present:
- The script file path (
~/Desktop/Reels/scripts/..., or your chosen vault folder)
- The analysis file path (
~/Desktop/Reels/analyses/...)
- The downloaded video path (
~/Desktop/Reels/downloads/...)
- Caption + comment trigger + deliverable, paste-ready
- On-screen overlays, paste-ready
That's the whole job. No library automation, no multi-channel distribution, no auto-posting — just the script.