Paste a reel URL. Get back the transcript, a breakdown of why the video performed, three hook options, a full word-for-word imitation script, and filming notes — all saved to a Notion page. Runs locally on your Mac. No OpenAI key, no usage fees.
If you'd rather not read the whole guide, skip to the bottom — there's a one-shot setup prompt you paste into Claude Code.
What this is
This is the upgraded version of the basic transcribe pipeline. The basic version gives you a transcript. This version gives you a transcript + a structured breakdown that helps you actually use the video as a template.
The output is a single Notion page with four sections:
- Source URL — link back to the original
- Original Transcript — the full Whisper output
- Why This Worked — 3-5 bullets covering hook, structure, pacing, CTA
- Imitation Script — three hook options, the chosen hook, a word-for-word spoken script, and filming notes
Page title is the chosen hook. Status, priority, and platform tag are pre-filled so it slots straight into your content board.
Architecture
You paste URL into Claude Code
|
v
/transcribe slash command
|
v
yt-dlp (download audio)
|
v
ffmpeg (16kHz mono WAV)
|
v
mlx-whisper (local Whisper, Apple Silicon)
|
v
claude --print (Sonnet 4.6) <-- brand voice + framework files (optional)
|
v
POST /v1/pages -> Notion content boardFive components: a slash command, a shell orchestrator, two Python helpers, and an optional brand voice file. Everything sits in ~/.claude/.
Prerequisites
- Apple Silicon Mac (M1/M2/M3/M4)
- Claude Code installed (
claude --versionworks) - Homebrew installed
- A Notion workspace and a database to post to
- ~10 minutes for setup
Part 1: Install dependencies
brew install yt-dlp ffmpeg
pip3 install --break-system-packages mlx-whisperVerify:
yt-dlp --version
ffmpeg -version | head -1
python3 -c "import mlx_whisper; print('ok')"Part 2: Set up Notion
2a. Create the database
In Notion, create a new database (full page). Add these properties:
| Name | Type | Notes |
|---|---|---|
| Task name | Title | Default — the chosen hook goes here |
| Status | Status | Add option "Not started" |
| Priority | Select | Add option "High" |
| Task type | Multi-select | Add options "Reel/Short" and "Youtube" |
You can rename "Task name" to "Title" if you prefer — just update the corresponding key in transcribe-notion-post.py later.
2b. Create the integration
- Go to notion.so/my-integrations
- Click "New integration"
- Name it (e.g. "Video Deconstructor"), select your workspace, click Save
- Copy the Internal Integration Token — looks like
ntn_...
2c. Connect it to your database
- Open your database in Notion
- Click the
...menu in the top right -> Connections -> add your integration - Grab the database ID from the URL:
https://www.notion.so/<DATABASE_ID>?v=...(the 32-char string before the?)
You now have a token and a database ID. Keep them handy.
Part 3: Create the orchestrator script
mkdir -p ~/.claude/tools ~/.claude/commandsCreate ~/.claude/tools/transcribe-video.sh:
#!/bin/bash
set -euo pipefail
URL=""
EXTRA_CONTEXT=""
SEND_NOTION=true
while [[ $# -gt 0 ]]; do
case "$1" in
--no-notion) SEND_NOTION=false; shift ;;
--context) EXTRA_CONTEXT="${2:-}"; shift 2 ;;
-*) echo "Unknown flag: $1" >&2; exit 1 ;;
*) URL="$1"; shift ;;
esac
done
if [[ -z "$URL" ]]; then
echo "Usage: transcribe-video.sh <URL> [--context \"...\"] [--no-notion]" >&2
exit 1
fi
YTDLP="$(command -v yt-dlp)"
FFMPEG="$(command -v ffmpeg)"
TOOLS="$HOME/.claude/tools"
# Notion config — read from env
NOTION_TOKEN="${NOTION_TOKEN:-}"
NOTION_DB="${NOTION_DB:-}"
NOTION_API="https://api.notion.com/v1/pages"
NOTION_VERSION="2022-06-28"
if [[ "$SEND_NOTION" == "true" && ( -z "$NOTION_TOKEN" || -z "$NOTION_DB" ) ]]; then
echo "Set NOTION_TOKEN and NOTION_DB in your shell or pass --no-notion" >&2
exit 1
fi
TMPDIR_WORK="$(mktemp -d)"
trap 'rm -rf "$TMPDIR_WORK"' EXIT
detect_platform() {
local url="$1"
if [[ "$url" =~ (youtube\.com|youtu\.be) ]]; then echo "Youtube"
elif [[ "$url" =~ instagram\.com ]]; then echo "Reel/Short"
elif [[ "$url" =~ tiktok\.com ]]; then echo "Reel/Short"
else echo "Youtube"
fi
}
TASK_TYPE="$(detect_platform "$URL")"
echo "Fetching title..." >&2
VIDEO_TITLE="$("$YTDLP" --print title "$URL" 2>/dev/null || echo "Unknown Title")"
echo "Title: $VIDEO_TITLE" >&2
echo "Downloading audio..." >&2
"$YTDLP" -x --audio-format wav -o "$TMPDIR_WORK/audio_raw.%(ext)s" "$URL" 2>&1 | tail -3 >&2
AUDIO_FILE="$(find "$TMPDIR_WORK" -name 'audio_raw.*' -type f | head -1)"
[[ -z "$AUDIO_FILE" ]] && { echo "Audio download failed" >&2; exit 1; }
AUDIO_WAV="$TMPDIR_WORK/audio_16k.wav"
"$FFMPEG" -y -i "$AUDIO_FILE" -ar 16000 -ac 1 -c:a pcm_s16le "$AUDIO_WAV" 2>/dev/null
echo "Transcribing..." >&2
TRANSCRIPT="$(AUDIO_PATH="$AUDIO_WAV" python3 -c "
import mlx_whisper, os
r = mlx_whisper.transcribe(os.environ['AUDIO_PATH'], path_or_hf_repo='mlx-community/whisper-base-mlx')
print(r['text'].strip())
")"
[[ -z "$TRANSCRIPT" ]] && { echo "Transcription empty" >&2; exit 1; }
echo "Got ${#TRANSCRIPT} chars" >&2
echo "Building analysis prompt..." >&2
export _TV_URL2="$URL" _TV_TITLE2="$VIDEO_TITLE" _TV_TRANSCRIPT2="$TRANSCRIPT"
export _TV_CONTEXT2="$EXTRA_CONTEXT" _TV_PROMPT_OUT="$TMPDIR_WORK/prompt.txt"
python3 "$TOOLS/transcribe-build-prompt.py"
echo "Generating analysis..." >&2
FULL_OUTPUT="$(claude --dangerously-skip-permissions --no-session-persistence \
--print --model claude-sonnet-4-6 \
-- "$(cat "$TMPDIR_WORK/prompt.txt")" 2>/dev/null)"
if [[ -z "$FULL_OUTPUT" || "${#FULL_OUTPUT}" -lt 100 ]]; then
echo "Analysis empty — saving transcript only" >&2
FULL_OUTPUT=""
fi
if [[ "$SEND_NOTION" == "true" ]]; then
echo "Posting to Notion..." >&2
export _TV_URL="$URL" _TV_TITLE="$VIDEO_TITLE" _TV_TASK_TYPE="$TASK_TYPE"
export _TV_TRANSCRIPT="$TRANSCRIPT" _TV_FULL_OUTPUT="$FULL_OUTPUT"
export _TV_NOTION_TOKEN="$NOTION_TOKEN" _TV_NOTION_DB="$NOTION_DB"
export _TV_NOTION_API="$NOTION_API" _TV_NOTION_VERSION="$NOTION_VERSION"
NOTION_URL="$(python3 "$TOOLS/transcribe-notion-post.py")"
[[ -n "$NOTION_URL" ]] && echo "Notion: $NOTION_URL" >&2
fi
echo ""
echo "=== $VIDEO_TITLE ==="
echo ""
echo "$TRANSCRIPT"
echo ""
echo "---"
echo ""
echo "$FULL_OUTPUT"Make it executable:
chmod +x ~/.claude/tools/transcribe-video.shPart 4: Create the prompt builder
Create ~/.claude/tools/transcribe-build-prompt.py:
#!/usr/bin/env python3
"""Build the Sonnet prompt for analysis + imitation script."""
import os
url = os.environ['_TV_URL2']
title = os.environ['_TV_TITLE2']
transcript = os.environ['_TV_TRANSCRIPT2']
extra = os.environ.get('_TV_CONTEXT2', '').strip()
out_file = os.environ['_TV_PROMPT_OUT']
base = os.path.expanduser('~/.claude')
def read_file(path):
try:
with open(path) as f: return f.read().strip()
except: return ''
# Optional brand customisation — drop a brand/voice.md to personalise the script
voice = read_file(base + '/brand/voice.md')
parts = [
"You are deconstructing a viral video and writing an imitation script.",
"",
f"Source URL: {url}",
f"Title: {title}",
"",
"TRANSCRIPT:",
transcript,
"",
"---",
]
if extra:
parts += [
"## ANGLE — adapt the imitation around this topic",
"",
"The viral structure comes from the source video. The SUBJECT and CONCRETE EXAMPLES of the imitation must come from this brief:",
"",
extra,
"",
"---",
]
if voice:
parts += [
"## BRAND VOICE — write in this voice",
"",
voice,
"",
"---",
]
parts += [
"## TASK",
"",
"First: analyse why this video performed well — hook, structure, pacing, retention beats, CTA.",
"Then: produce a full imitation script. Pick the format that matches (contrarian take, before/after, experiment, hidden truth, etc.).",
"",
"## OUTPUT FORMAT (exact, no deviations)",
"",
"WHY THIS WORKED:",
"[3-5 bullets]",
"",
"HOOK 1: [7 words max]",
"HOOK 2: [7 words max]",
"HOOK 3: [7 words max]",
"",
"CHOSEN HOOK: [restate the best one]",
"",
"SPOKEN SCRIPT:",
"[Full word-for-word script. End on an open loop or CTA.]",
"",
"FILMING NOTES:",
"[2-4 lines. Tone, pacing, key emphasis moments.]",
]
with open(out_file, 'w') as f:
f.write('\n'.join(parts))Part 5: Create the Notion poster
Create ~/.claude/tools/transcribe-notion-post.py:
#!/usr/bin/env python3
"""Post transcript + analysis to a single Notion page."""
import json, os, sys, urllib.request, re
url = os.environ["_TV_URL"]
title = os.environ["_TV_TITLE"]
task_type = os.environ["_TV_TASK_TYPE"]
transcript = os.environ["_TV_TRANSCRIPT"]
full_output = os.environ.get("_TV_FULL_OUTPUT", "")
notion_token = os.environ["_TV_NOTION_TOKEN"]
notion_db = os.environ["_TV_NOTION_DB"]
notion_api = os.environ["_TV_NOTION_API"]
notion_version = os.environ["_TV_NOTION_VERSION"]
def chunk(text, size=2000):
return [text[i:i+size] for i in range(0, len(text), size)] or [""]
def para(text):
return {"object": "block", "type": "paragraph", "paragraph": {
"rich_text": [{"type": "text", "text": {"content": text}}]
}}
def heading(text):
return {"object": "block", "type": "heading_2", "heading_2": {
"rich_text": [{"type": "text", "text": {"content": text}}]
}}
def divider():
return {"object": "block", "type": "divider", "divider": {}}
# Title = chosen hook if available
page_title = f"Transcript: {title[:200]}"
if full_output:
m = re.search(r"CHOSEN HOOK:\s*(.+)", full_output)
if m: page_title = m.group(1).strip()[:200]
children = [para(f"Source: {url}"), divider(), heading("Original Transcript")]
for c in chunk(transcript): children.append(para(c))
if full_output:
children += [divider(), heading("Why This Worked")]
why = re.search(r"WHY THIS WORKED:(.*?)(?=HOOK 1:|$)", full_output, re.DOTALL)
if why:
for c in chunk(why.group(1).strip()): children.append(para(c))
children += [divider(), heading("Imitation Script")]
script = re.search(r"(HOOK 1:.*)", full_output, re.DOTALL)
body = script.group(1).strip() if script else full_output
for c in chunk(body): children.append(para(c))
children = children[:100]
payload = {
"parent": {"database_id": notion_db},
"properties": {
"Task name": {"title": [{"text": {"content": page_title}}]},
"Status": {"status": {"name": "Not started"}},
"Priority": {"select": {"name": "High"}},
"Task type": {"multi_select": [{"name": task_type}]},
},
"children": children,
}
req = urllib.request.Request(
notion_api, data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": f"Bearer {notion_token}",
"Content-Type": "application/json",
"Notion-Version": notion_version,
},
method="POST",
)
try:
resp = urllib.request.urlopen(req)
print(json.loads(resp.read()).get("url", ""))
except Exception as e:
print(f"NOTION_ERROR: {e}", file=sys.stderr)Part 6: Create the slash command
Create ~/.claude/commands/transcribe.md:
---
name: transcribe
description: Transcribe a video URL and post analysis + imitation script to Notion
user-invocable: true
---
Transcribe a video. The user provided: $ARGUMENTS
Parse $ARGUMENTS:
- Find the URL (the http(s)://... token)
- Everything else is EXTRA_CONTEXT — the angle they want the imitation built around. Trim filler like "transcribe this:".
Run the script. With context:
\``bash
bash ~/.claude/tools/transcribe-video.sh "<URL>" --context "<EXTRA_CONTEXT>"
\`
Without context:
\`bash
bash ~/.claude/tools/transcribe-video.sh "<URL>"
\``
After the command completes, the transcript and analysis are printed to stdout. Tell the user "Transcribed and saved to Notion." If you passed extra context, add: "Anchored the imitation to: [short paraphrase]."
If $ARGUMENTS is empty, ask for a URL.(Strip the backslashes before the triple backticks — they're escaping for this guide only.)
Part 7: Set Notion credentials
Add to your ~/.zshrc (or ~/.bashrc):
export NOTION_TOKEN="ntn_your_token_here"
export NOTION_DB="your_database_id_here"Then source ~/.zshrc (or restart your terminal).
Part 8: Test it
Restart Claude Code so it picks up the new slash command. Then:
/transcribe https://www.instagram.com/reel/<some-id>/With angle:
/transcribe https://www.instagram.com/reel/<some-id>/ make this about how juniors should learn to code with AIYou'll see progress in the terminal:
Title: <video title>
Downloading audio...
Got 1247 chars
Building analysis prompt...
Generating analysis...
Notion: https://www.notion.so/<page-id>A new page lands in your Notion content board with the four sections.
Optional: Customise the voice
Drop a markdown file at ~/.claude/brand/voice.md describing how you write — phrases you use, ones you don't, tone, sentence length, audience. The prompt builder picks it up automatically and the imitation script will sound like you.
Example:
# My Voice
- Australian, terse, no corporate filler
- I write for solo builders and creators, not enterprise
- Short sentences. No "in today's world" openings.
- I never say "leverage", "synergy", "solutions", "dive deep"
- Hooks land hard or get cutIf the file doesn't exist, the script falls back to a generic creator voice.
One-shot setup prompt
Paste this into Claude Code and it'll build the whole thing. Have your Notion token and database ID ready — Claude will ask for them.
Set up the viral video deconstructor on this Mac. End state: I run /transcribe <url> in Claude Code and a full breakdown lands in Notion.
What I need:
1. Install yt-dlp, ffmpeg via Homebrew, and mlx-whisper via pip3 (use --break-system-packages)
2. Ask me for my Notion integration token (ntn_...) and database ID, then add NOTION_TOKEN and NOTION_DB exports to my ~/.zshrc
3. Create ~/.claude/tools/transcribe-video.sh — a bash orchestrator that:
- Takes a URL, optional --context "...", optional --no-notion
- Detects platform (Youtube, Reel/Short)
- yt-dlp downloads audio as wav, ffmpeg converts to 16kHz mono PCM
- mlx-whisper transcribes locally with mlx-community/whisper-base-mlx
- Calls a prompt builder, then runs claude --print --model claude-sonnet-4-6 to generate analysis
- POSTs to Notion (single page with Source / Transcript / Why This Worked / Imitation Script sections)
- Prints transcript + full analysis to stdout
4. Create ~/.claude/tools/transcribe-build-prompt.py — builds the Sonnet prompt; reads ~/.claude/brand/voice.md if it exists; injects extra context if passed; outputs in this exact format: WHY THIS WORKED bullets, HOOK 1/2/3, CHOSEN HOOK, SPOKEN SCRIPT, FILMING NOTES
5. Create ~/.claude/tools/transcribe-notion-post.py — POSTs one page to Notion. Title = chosen hook. Properties: Task name (title), Status="Not started", Priority="High", Task type=Reel/Short or Youtube. Body sections in order: Source URL, Original Transcript, Why This Worked, Imitation Script. Chunk text into 2000-char blocks, cap children at 100.
6. Create ~/.claude/commands/transcribe.md — slash command with frontmatter (name: transcribe, user-invocable: true). Parses $ARGUMENTS into URL + extra context, runs the bash script, reports back.
7. chmod +x the bash script
8. Test by running /transcribe https://www.youtube.com/watch?v=jNQXAC9IVRw
My Notion database has these properties already: Task name (title), Status (status with "Not started" option), Priority (select with "High" option), Task type (multi-select with "Reel/Short" and "Youtube" options). If mine doesn't, tell me what to add.
Use the architecture from this guide: https://[wherever you publish this]That's the full system. From this point you can:
- Drop any reel link and have the script written for you in 60 seconds
- Build a swipe file of viral structures by category in Notion
- Add your brand voice file to make the imitation sound like you
- Wire it into a Telegram bot if you want to send links from your phone (separate guide)