Type one idea. Get back a finished video of your AI avatar, with captions, scheduled to five apps. Claude can build it with you.
Swipe or tap Next · checked on 2026-10-04
This is the way I'd do it. You paste one prompt into Claude Code, and Claude turns into your setup helper. It asks you questions one at a time, tells you exactly where to click, and builds the whole thing with you. Your job is answering questions and clicking a few buttons. Claude does the typing.
That's $73 a month at list prices, checked on 2026-10-04. Prices change, so check each page before you buy.
Three quick steps. You'll paste a couple of lines into the terminal (the window where you type commands). Never opened one? Anthropic's terminal guide for new users walks you through it.
Terminal, and press Return.wsl --install and press Enter. Restart when it's done. Then open Ubuntu from the Start menu and pick a username and password. Use Ubuntu from here on (Microsoft's steps).Paste this line and press Enter. (Mac: Command + V. Ubuntu or Linux: Ctrl + Shift + V.) When it's done, close the window and open a new one.
Install (Mac, Linux, or Ubuntu on Windows)
curl -fsSL https://claude.ai/install.sh | bashThis makes an empty folder, steps into it, and starts Claude. The first time, your browser opens so you can log in. If it asks whether you trust this folder, say yes.
mkdir -p ~/content-engine
cd ~/content-engine
claudeClick Copy, paste it into Claude Code, and press Enter. Claude Code may show it as one short line that says Pasted text. That's normal. It's long on purpose: it tells Claude how to talk to you, what it must never do, and the order to build things in. You don't have to read all of it. The safety rules are worth a look, though.
The setup prompt (copy all of it) · lines 1-3 of 80. Copy all copies the whole prompt; read it in full on the page.
Hi Claude. Please be my patient setup helper for the next few hours.
I want to build a simple content engine in this folder. I type one idea. It makes a short vertical video of my AI avatar (a talking video of me, in my own voice) saying it, adds captions, and schedules it to my social accounts. I don't know how to code, so treat me like a smart beginner.Hi Claude. Please be my patient setup helper for the next few hours.
I want to build a simple content engine in this folder. I type one idea. It makes a short vertical video of my AI avatar (a talking video of me, in my own voice) saying it, adds captions, and schedules it to my social accounts. I don't know how to code, so treat me like a smart beginner.
THE GUIDE
We're building the starter engine from this guide:
https://receiptsgroup.com/guides/ai-content-engine
Its "manual way" sections are the source of truth for every file, command and menu path. When you need a file, download the page with curl (ask me first) and copy that file exactly from its code box. Each box is labeled with its file name, like engine/check.py. Don't rewrite the scripts. Only fill in my details where the guide shows <angle brackets>. If you can't reach the page, tell me before you build anything from memory. If a website or command looks different from the guide, tell me, and check the official page the guide links to.
HOW TO TALK TO ME
- Plain words and short sentences. If you need a tech word, explain it in five words or less.
- Ask ONE question at a time. Then stop and wait for my answer.
- Before each step, tell me in one sentence what it does and why.
- Before each command, tell me in plain words what it does, so I know what I'm saying yes to.
- Tell me exactly where to click and what I should see. If something looks different, ask me what's on my screen.
- If I'm stuck, slow down and try another way. Never make me feel dumb.
SAFETY RULES (never break these)
1. Never ask me to paste a password, API key or secret into this chat. If I paste one by accident, tell me to delete that key and make a new one.
2. Keys go only in a file called .env in this folder. You make it with the names and blank values. I type the values in myself. Never show what's inside .env. To check that a value is filled in, use a command that only prints yes or no.
3. Ask me first before anything that costs money or credits, and before anything that posts in public. Tell me what it costs or does. Do the free dry run first, every time.
4. AI disclosure stays on. Every video and every caption says it was made with an AI avatar.
5. My avatar is only me: my own face and my own voice, with my OK. Never anyone else's.
6. Stay in this folder. Ask before you install anything, and tell me what it is in plain words.
7. No made-up numbers, results, promises or quotes in my scripts or captions.
THE PLAN
Go one step at a time. After each step, if there's something to test, run a free test that spends nothing and tell me in one line if it passed. Keep a short checklist in SETUP-NOTES.md of what's done (no keys in it). If we stop partway, read it first next time and pick up where we left off.
Step 1. Get to know me.
Say hi. In two or three short sentences, tell me what we'll do and that it takes about an afternoon. Then ask me these, one at a time:
a) What do you do, and who do you talk to?
b) What job title and business name should your videos use?
c) How do you talk? (Offer to learn my voice from 3 to 5 sentences I paste in: a text, an email, a post, or how I'd explain my work out loud.)
d) Anything you never want said? Words you hate, topics to skip?
e) Which apps do you want to post to: YouTube, Facebook, Instagram, TikTok, Threads?
f) Which of these do you already have: a paid Claude plan, HeyGen, Upload-Post?
Check for yourself which computer I'm on (Mac, Windows or Linux). Only ask if you can't tell.
Then sum up my answers in a few lines and ask if you got it right.
Step 2. The shopping list.
Give me a short checklist of only what I still need, with the link and the plan to pick:
- Claude Pro or higher, $20/mo: https://claude.com/pricing (I'm probably on it already, since I'm talking to you.)
- HeyGen Creator, $29/mo, for my avatar and my voice: https://app.heygen.com (plans: https://www.heygen.com/pricing)
- Upload-Post Basic, $24/mo, to schedule my posts (the free plan can't post to TikTok): https://upload-post.com (plans: https://docs.upload-post.com/resources/pricing-and-limits)
- HyperFrames, the video editor: free. Nothing to sign up for.
Say these are list prices checked on 2026-10-04, and I should check each page. Remind me that Instagram must be a Business or Creator account, and that Facebook posts go to a Page. Then wait while I sign up. Ask me to type "done" for each one.
Step 3. Get my computer ready. (Guide: "Setup, step by step", steps 1, 2 and 4.)
On Windows, this whole build runs inside WSL (Windows' built-in Linux), because HeyGen's tool needs it. If I'm not in WSL, help me get there first.
Check what I already have: Node.js 22 or newer, FFmpeg, Python 3. Install only what's missing, one at a time, after you ask. Make the subfolders from the guide (rules, prompts, engine, runs). Add the HyperFrames plugin to Claude Code. Test: the version checks and npx hyperframes doctor.
Step 4. My avatar and my voice. (Guide: Setup step 5.)
First, ask me to confirm that the photo and the voice will be mine, and that I'm OK with HeyGen using them. Then walk me through HeyGen's site, one click at a time: Avatars, New Avatar, Upload Photo (a clear photo of me, eyes and mouth easy to see). Then Voice, + New Voice, Create New Voice, Instant Voice Cloning (I record in a quiet room). HeyGen takes a while to check the avatar, so move on to step 5 while it does.
Step 5. Build the engine. (Guide: Setup steps 3 and 8.)
Make the files from the guide: CLAUDE.md, rules/VOICE.md, rules/GUARDRAILS.md, rules/banned.txt, prompts/01-script.md, prompts/02-edit.md, prompts/03-captions.md, engine/check.py, engine/avatar.py and engine/post.py. Write VOICE.md and GUARDRAILS.md from my answers, in my words, and swap the guide's example lines for mine. Show me VOICE.md and ask if it sounds like me. Test: the guide's Test 1 (do you follow my rules?).
Step 6. Keys and IDs, one at a time. (Guide: Setup steps 6 and 7, and "Environment variables".)
Make .env with these names and blank values, plus .env.example and .gitignore from the guide:
UPLOAD_POST_API_KEY=
UPLOAD_POST_PROFILE=
FACEBOOK_PAGE_ID=
HEYGEN_AVATAR_ID=
HEYGEN_VOICE_ID=
Open .env for me in a simple text editor. Then, for each value: tell me where to click to get it and which line it goes on (right after the = sign, no spaces, no quotes). Wait while I paste it and save. Check it with your yes/no command, then test it.
- HeyGen: install the HeyGen CLI (ask first) and log in with heygen auth login --oauth. It opens my browser. My avatar and voice IDs aren't secret, so find them with heygen avatar looks list and heygen voice list, show me which ones are mine, and I'll paste them in.
- Upload-Post: in Manage Users, I make a profile (that name goes on UPLOAD_POST_PROFILE) and connect each app I picked. Then API Keys, Generate New API Key, and I paste it into .env. Test it with the guide's "me" call. It shows my email and plan, never the key.
- FACEBOOK_PAGE_ID: only if I post to Facebook and manage more than one Page. Use the guide's Facebook Pages call and show me the list.
Step 7. Free tests. (Guide: "First test".)
Run the guide's Test 2: a test script, the guardrail check, the avatar dry run, the HyperFrames check and the Upload-Post check. None of these spend anything. Explain each result in one plain line. Fix anything that fails before we spend a cent.
Step 8. My first video. (Guide: "One video, start to finish".)
Ask me for one idea, in my own words. Write the script. I read it and say OK. Ask before the avatar render (it uses HeyGen credits). Build the edit, then have me watch the whole video with sound. Write the captions and run the check. Show me the post dry run. Ask me before you schedule anything. The AI label stays on.
Step 9. Wrap up.
Give me one short paragraph on how to make a video every day from now on: what I type, and the two things I always check myself (the script and the finished video). Then a short cheat sheet of the commands, in order.
Ready? Say hi and ask me your first question..env, opens it for you, and tells you where to get each key and which line to paste it on. Keys go in that file. Never in the chat.How long: plan on an afternoon. Most of it is waiting: HeyGen checking your avatar, each app confirming its connection, and the first video render.
What you'll have at the end: a folder on your computer that turns one idea into a finished video. Your first video, made and (if you said yes) scheduled, with the AI label on. And a short note from Claude on how to make the next one.
Need a break? Just close the window. When you're back, open the terminal and paste these two lines. Claude picks up where you left off. It keeps a checklist in SETUP-NOTES.md, too.
Pick up where you left off
cd ~/content-engine
claude --continueWant to see every step yourself? The manual way is below.
This is the manual way: every step, every file and every command. Use it to do the whole thing by hand, or keep it open to check what Claude is doing. It's also what the setup prompt tells Claude to follow.
Here's the whole chain. Each box is one tool doing one job, and each one leaves a file behind. So when something comes out weird (it will), you can open the files in order and see exactly which step did it.
claude -p "...". One instruction in, one result out, no chat window. That's what lets a script call it.heygen; HyperFrames runs as npx hyperframes.UPLOAD_POST_API_KEY. Ours live in a file called .env that never gets shared.The avatar is you: your own photo and your own voice, with your consent. Not a client, not a friend who said “sure, whatever,” not a celebrity. HeyGen's own docs say it runs no consent check on photo avatars, and that getting the person's agreement and keeping a record of it is your job. So keep it simple: only ever you.
Four tools plus some plumbing. Here's what each one does in this build, which plan actually unlocks the part we use, and what it costs. Prices are list prices on 2026-10-04, US dollars.
HeyGen also bills yearly; check its pricing page for that figure.
Plan an afternoon for setup. Most of it is waiting: HeyGen validating your avatar, each social network confirming its connection, the first render. After that, a video is mostly machine time. Your part is reading the script and watching the cut, which, by the way, are the two parts you should never automate.
runs/<name>/final.mp4.Do these in order. Every command goes in the terminal unless I say otherwise, and anything in <angle brackets> is yours to fill in.
HyperFrames needs Node.js 22 or newer plus FFmpeg; the glue scripts need Python 3. On nodejs.org, take the LTS download (version 24 on the day I checked). FFmpeg's download page lists builds for macOS and Windows and the packages for Linux. Check first, though: you may already have some of these.
Check all three
node --version # want v22 or higher
ffmpeg -version # prints a version banner
python3 --version # the scripts were tested on 3.9; newer is fineYou need Claude Pro, Max, Team or Enterprise, or an Anthropic Console account (the free plan doesn't include Claude Code). Install it, open a new terminal window, check the version, then start it once inside your new engine folder to log in.
Install (macOS, Linux, WSL)
curl -fsSL https://claude.ai/install.sh | bash
# open a NEW terminal window, then:
claude --version
claude doctor # read-only health checkMake the folder and log in (first time only)
mkdir -p ~/content-engine/rules ~/content-engine/prompts ~/content-engine/engine ~/content-engine/runs
cd ~/content-engine
claude
# follow the browser login, then type /exitThis is the whole repo. Small on purpose. Claude Code reads CLAUDE.md at the start of every session, headless ones included, and CLAUDE.md can pull in other files with @path lines. So your rules ride along on every call without you pasting them.
Save each block below as the file named on its label. Fill in the <angle brackets>. The before→after examples in VOICE.md matter more than anything else here: replace mine with three to five of your own.
rules/banned.txt
# One phrase per line. engine/check.py fails any script or caption that uses one.
guaranteed
game-changer
in today's video
in today's fast-paced world
studies show
link in bio.env.example (then copy it to .env)
# Copy to .env and fill in. Never commit, paste or screen-share .env.
UPLOAD_POST_API_KEY=
UPLOAD_POST_PROFILE=
FACEBOOK_PAGE_ID=
HEYGEN_AVATAR_ID=
HEYGEN_VOICE_ID=.gitignore
.env
runs/Repo layout · Part 1 of 2 · Copy copies all 26 lines
content-engine/
├── CLAUDE.md loads your rules into every Claude Code run
├── rules/
│ ├── VOICE.md how you talk
│ ├── GUARDRAILS.md what never goes out
│ └── banned.txt phrases check.py fails on
├── prompts/
│ ├── 01-script.md thought -> script.json
│ ├── 02-edit.md avatar.mp4 -> final.mp4 (HyperFrames)
│ └── 03-captions.md script -> captions.json, one per platform
├── engine/
│ ├── check.py the guardrail check
│ ├── avatar.py HeyGen render (Avatar IV)
│ └── post.py Upload-Post scheduling, dry run by default
├── .env your keys and ids (never shared)content-engine/
├── CLAUDE.md loads your rules into every Claude Code run
├── rules/
│ ├── VOICE.md how you talk
│ ├── GUARDRAILS.md what never goes out
│ └── banned.txt phrases check.py fails on
├── prompts/
│ ├── 01-script.md thought -> script.json
│ ├── 02-edit.md avatar.mp4 -> final.mp4 (HyperFrames)
│ └── 03-captions.md script -> captions.json, one per platform
├── engine/
│ ├── check.py the guardrail check
│ ├── avatar.py HeyGen render (Avatar IV)
│ └── post.py Upload-Post scheduling, dry run by default
├── .env your keys and ids (never shared)
├── .env.example the names, no values
├── .gitignore keeps .env and runs/ out of git
└── runs/
└── 2026-10-06-retest/ one folder per video
├── thought.txt
├── script.json
├── avatar.mp4
├── edit/ the HyperFrames project
├── final.mp4
├── captions.json
└── sent.json Upload-Post's replies (job ids)Repo layout · Part 2 of 2 · Copy copies all 26 lines
├── .env.example the names, no values
├── .gitignore keeps .env and runs/ out of git
└── runs/
└── 2026-10-06-retest/ one folder per video
├── thought.txt
├── script.json
├── avatar.mp4
├── edit/ the HyperFrames project
├── final.mp4
├── captions.json
└── sent.json Upload-Post's replies (job ids)CLAUDE.md
# My content engine
This folder turns one thought into a short vertical video and a scheduled post.
Every script, caption and word on screen follows these files:
@rules/VOICE.md
@rules/GUARDRAILS.md
Work only inside this folder. Never open, print or copy .env.# My content engine
This folder turns one thought into a short vertical video and a scheduled post.
Every script, caption and word on screen follows these files:
@rules/VOICE.md
@rules/GUARDRAILS.md
Work only inside this folder. Never open, print or copy .env.rules/VOICE.md · Part 1 of 2 · Copy copies all 17 lines
# How I talk
Who's talking: <your job title> at <your company>, explaining one idea to one
person, the way I would on a call. Casual, plain words, short sentences, a little
dry humor where it fits. Not a training video. Not a stand-up set.
- Open with the point or a question. Never "In today's video".
- One idea per video. Two ideas means two videos.
- Explain any technical word the first time it comes up, in plain words.
- End on the lesson, or a question to the viewer.
## Before -> how I'd actually say it
Replace these with 3-5 of your own. They teach Claude more than any adjective.
- "Leverage AI to streamline your operations."
-> "I let the AI do the boring first draft. I still read it."# How I talk
Who's talking: <your job title> at <your company>, explaining one idea to one
person, the way I would on a call. Casual, plain words, short sentences, a little
dry humor where it fits. Not a training video. Not a stand-up set.
- Open with the point or a question. Never "In today's video".
- One idea per video. Two ideas means two videos.
- Explain any technical word the first time it comes up, in plain words.
- End on the lesson, or a question to the viewer.
## Before -> how I'd actually say it
Replace these with 3-5 of your own. They teach Claude more than any adjective.
- "Leverage AI to streamline your operations."
-> "I let the AI do the boring first draft. I still read it."
- "Consistency is key to social media success."
-> "Posting twice and vanishing for a month mostly trains people not to wait for you."rules/VOICE.md · Part 2 of 2 · Copy copies all 17 lines
- "Consistency is key to social media success."
-> "Posting twice and vanishing for a month mostly trains people not to wait for you."rules/GUARDRAILS.md
# Never publish
- I'm the <exact title> of <company>. Never call me anything else.
- I appear only as my own avatar, in my own voice. Nobody else's face, voice or name.
- No numbers stated as facts unless they're in my thought. No "studies show".
- No income, revenue or results promises. No guarantees. No testimonials.
- No client names, customer details, or anything from a real account or screen.
- Every video says it was made with an AI avatar (on screen and in the caption).
- Scripts run 60-110 words. Banned phrases live in rules/banned.txt, and
engine/check.py fails anything that uses one.# Never publish
- I'm the <exact title> of <company>. Never call me anything else.
- I appear only as my own avatar, in my own voice. Nobody else's face, voice or name.
- No numbers stated as facts unless they're in my thought. No "studies show".
- No income, revenue or results promises. No guarantees. No testimonials.
- No client names, customer details, or anything from a real account or screen.
- Every video says it was made with an AI avatar (on screen and in the caption).
- Scripts run 60-110 words. Banned phrases live in rules/banned.txt, and
engine/check.py fails anything that uses one.HyperFrames is HeyGen's open-source “write HTML, render video” framework. The plugin teaches Claude Code how to build a valid HyperFrames project; the CLI renders it on your machine. The first npx run downloads the CLI, so give it a minute.
claude plugin update hyperframes@hyperframes now and then.Install and check
claude plugin marketplace add heygen-com/hyperframes
claude plugin install hyperframes@hyperframes
claude plugin details hyperframes # should list hyperframes, general-video and media-use
npx hyperframes doctor # checks Node.js, FFmpeg and Chromeclaude plugin marketplace add heygen-com/hyperframes
claude plugin install hyperframes@hyperframes
claude plugin details hyperframes # should list hyperframes, general-video and media-use
npx hyperframes doctor # checks Node.js, FFmpeg and ChromeSign up and pick a plan (Creator is where voice cloning and 1080p show up). Then:
Want a different outfit or setting later? Generate a new look from your own photo (HeyGen lists one credit per look). New looks of you: fine. Anybody else's face: no.
The CLI is how avatar.py talks to HeyGen. It can log in two ways, and they bill differently:
heygen auth login --oauth): a browser sign-in; renders use your subscription credits. HeyGen's CLI readme describes this route for “Pro / Max subscription users”; it works on my Creator plan.Pick one; the CLI keeps one login at a time. Then copy your look id and your voice id into .env as HEYGEN_AVATAR_ID and HEYGEN_VOICE_ID. If avatar looks list asks for more, heygen avatar looks list --help shows the options.
Install, log in, find the ids
curl -fsSL https://static.heygen.ai/cli/install.sh | bash
heygen auth login --oauth # or: heygen auth login --api-key
heygen auth status # which login is active
heygen avatar looks list # JSON: find your photo avatar by name, copy its look id
heygen voice list # JSON: find your cloned voice, copy its id
heygen video create --request-schema # every field a render request acceptscurl -fsSL https://static.heygen.ai/cli/install.sh | bash
heygen auth login --oauth # or: heygen auth login --api-key
heygen auth status # which login is active
heygen avatar looks list # JSON: find your photo avatar by name, copy its look id
heygen voice list # JSON: find your cloned voice, copy its id
heygen video create --request-schema # every field a render request acceptsuser; put it in .env as UPLOAD_POST_PROFILE..env as UPLOAD_POST_API_KEY. You won't need to look at it again, and you shouldn't.Two gotchas before you hit them. Instagram has to be a Business or Creator account; personal accounts can't post through the API. And Facebook posts go to a Page: if your login manages more than one, find the right Page id with the second call below and put it in .env as FACEBOOK_PAGE_ID.
Check the key, then list your Facebook Pages
set -a; source .env; set +a # load .env into this terminal window
curl -s -H "Authorization: Apikey $UPLOAD_POST_API_KEY" https://api.upload-post.com/api/uploadposts/me
curl -s -H "Authorization: Apikey $UPLOAD_POST_API_KEY" https://api.upload-post.com/api/uploadposts/facebook/pagesset -a; source .env; set +a # load .env into this terminal window
curl -s -H "Authorization: Apikey $UPLOAD_POST_API_KEY" https://api.upload-post.com/api/uploadposts/me
curl -s -H "Authorization: Apikey $UPLOAD_POST_API_KEY" https://api.upload-post.com/api/uploadposts/facebook/pagesThe prompts are plain-English instructions Claude follows. The scripts are the glue: one checks the guardrails, one calls HeyGen, one calls Upload-Post. Save each block as the file named on its label. Read every line; there's nothing clever in them, which is the point.
prompts/01-script.md · Part 1 of 2 · Copy copies all 16 lines
Write a short-video script from my thought. You're given a run folder (runs/<name>).
1. Read <run>/thought.txt. That's my idea, in my words.
2. Write the script I'll say to camera as my avatar, following CLAUDE.md and the
rules it imports:
- 60-110 words (about 25-45 seconds), one idea, plain words.
- The first sentence is the hook: the point, or a question.
- End on the lesson, or a question to the viewer.
- Keep my phrasing wherever it works. No numbers, names or results I didn't give you.
3. Write <run>/script.json with exactly these keys:
{"title": "the hook as a title, 60 characters max",
"script": "the full script as plain text",
"word_count": 0,
"notes": ["anything you changed or dropped from my thought, and why"]}Write a short-video script from my thought. You're given a run folder (runs/<name>).
1. Read <run>/thought.txt. That's my idea, in my words.
2. Write the script I'll say to camera as my avatar, following CLAUDE.md and the
rules it imports:
- 60-110 words (about 25-45 seconds), one idea, plain words.
- The first sentence is the hook: the point, or a question.
- End on the lesson, or a question to the viewer.
- Keep my phrasing wherever it works. No numbers, names or results I didn't give you.
3. Write <run>/script.json with exactly these keys:
{"title": "the hook as a title, 60 characters max",
"script": "the full script as plain text",
"word_count": 0,
"notes": ["anything you changed or dropped from my thought, and why"]}
Don't create or edit any other file. End with one line: DONE or FAILED <reason>.prompts/01-script.md · Part 2 of 2 · Copy copies all 16 lines
Don't create or edit any other file. End with one line: DONE or FAILED <reason>.prompts/02-edit.md · Part 1 of 2 · Copy copies all 21 lines
Use the /hyperframes:hyperframes skill. Finish the video for the run folder you're given (runs/<name>).
What's there:
- <run>/edit/ is a HyperFrames project made by `npx hyperframes init` from <run>/avatar.mp4
(my avatar talking, 1080x1920). The clip is placed; nothing is designed yet.
- <run>/edit/transcript.json has the speech timings from Whisper, if transcription worked.
- <run>/script.json has the title and the exact words.
Build:
1. Keep the avatar clip and its audio exactly as they are: no trims, no speed changes.
2. Readable captions in the lower third, a few words at a time, timed to the audio.
Use script.json for the exact words. Captions never cover my mouth or eyes.
3. A title card over the first 2 seconds with the title from script.json.
4. At most two simple graphics, only where they show what I'm saying at that moment
(a short checklist, a before/after, one key phrase). My face stays on screen at least half the time.Use the /hyperframes:hyperframes skill. Finish the video for the run folder you're given (runs/<name>).
What's there:
- <run>/edit/ is a HyperFrames project made by `npx hyperframes init` from <run>/avatar.mp4
(my avatar talking, 1080x1920). The clip is placed; nothing is designed yet.
- <run>/edit/transcript.json has the speech timings from Whisper, if transcription worked.
- <run>/script.json has the title and the exact words.
Build:
1. Keep the avatar clip and its audio exactly as they are: no trims, no speed changes.
2. Readable captions in the lower third, a few words at a time, timed to the audio.
Use script.json for the exact words. Captions never cover my mouth or eyes.
3. A title card over the first 2 seconds with the title from script.json.
4. At most two simple graphics, only where they show what I'm saying at that moment
(a short checklist, a before/after, one key phrase). My face stays on screen at least half the time.
5. A small "AI avatar" label in a top corner, visible the whole time.
Every word on screen follows CLAUDE.md.
Then run `npx hyperframes check <run>/edit` and fix every error it reports.
Render with `npx hyperframes render <run>/edit --output <run>/final.mp4`.
Don't touch files outside <run>. End with one line: DONE <path> or FAILED <reason>.prompts/02-edit.md · Part 2 of 2 · Copy copies all 21 lines
5. A small "AI avatar" label in a top corner, visible the whole time.
Every word on screen follows CLAUDE.md.
Then run `npx hyperframes check <run>/edit` and fix every error it reports.
Render with `npx hyperframes render <run>/edit --output <run>/final.mp4`.
Don't touch files outside <run>. End with one line: DONE <path> or FAILED <reason>.prompts/03-captions.md · Part 1 of 2 · Copy copies all 17 lines
Write the post captions for the finished video. You're given a run folder (runs/<name>).
Read <run>/script.json and follow CLAUDE.md. Write <run>/captions.json with exactly this shape:
{"youtube": {"title": "the hook, under 70 characters",
"description": "2-3 short lines, then: Made with my AI avatar.",
"tags": ["3-6 plain tags"]},
"facebook": {"title": "the hook",
"description": "1-3 short lines, then: (Made with my AI avatar.)"},
"instagram": {"caption": "hook line, one or two short lines, 3-5 hashtags at the end"},
"tiktok": {"caption": "hook line, one short line, 3-5 hashtags"},
"threads": {"caption": "one or two lines ending in a question, under 400 characters, no hashtags"}}Write the post captions for the finished video. You're given a run folder (runs/<name>).
Read <run>/script.json and follow CLAUDE.md. Write <run>/captions.json with exactly this shape:
{"youtube": {"title": "the hook, under 70 characters",
"description": "2-3 short lines, then: Made with my AI avatar.",
"tags": ["3-6 plain tags"]},
"facebook": {"title": "the hook",
"description": "1-3 short lines, then: (Made with my AI avatar.)"},
"instagram": {"caption": "hook line, one or two short lines, 3-5 hashtags at the end"},
"tiktok": {"caption": "hook line, one short line, 3-5 hashtags"},
"threads": {"caption": "one or two lines ending in a question, under 400 characters, no hashtags"}}
Write each one for its platform, not one caption pasted five times. No links in the Instagram
or TikTok captions (they aren't clickable there). No "follow for more" bait. Nothing that
isn't in the script.
Then run `python3 engine/check.py <run>` and fix captions.json until it prints PASS.
End with one line: DONE or FAILED <reason>.prompts/03-captions.md · Part 2 of 2 · Copy copies all 17 lines
Write each one for its platform, not one caption pasted five times. No links in the Instagram
or TikTok captions (they aren't clickable there). No "follow for more" bait. Nothing that
isn't in the script.
Then run `python3 engine/check.py <run>` and fix captions.json until it prints PASS.
End with one line: DONE or FAILED <reason>.engine/check.py · lines 1-12 of 66. Copy all copies the whole file; read it in full on the page.
#!/usr/bin/env python3
"""Guardrail check for one run. Prints PASS, or FAIL and every reason (exit code 1).
python3 engine/check.py runs/<name>
Checks script.json (length, banned phrases) and, once it exists, captions.json (banned phrases, caption
lengths, links where they can't be clicked). Edit the numbers and rules/banned.txt to fit you.
"""
import json
import re
import sys
from pathlib import Path#!/usr/bin/env python3
"""Guardrail check for one run. Prints PASS, or FAIL and every reason (exit code 1).
python3 engine/check.py runs/<name>
Checks script.json (length, banned phrases) and, once it exists, captions.json (banned phrases, caption
lengths, links where they can't be clicked). Edit the numbers and rules/banned.txt to fit you.
"""
import json
import re
import sys
from pathlib import Path
MIN_WORDS, MAX_WORDS = 60, 110 # about 25-45 seconds of speech
LIMITS = {"instagram": 2200, "tiktok": 2200, "threads": 500} # Upload-Post's character limits page
NO_LINKS = ("instagram", "tiktok") # links in these captions aren't clickable
URL = re.compile(r"https?://|www\.", re.I)
def size(platform, text):
if platform == "threads":
return len(text.encode("utf-8")) # Threads counts UTF-8 bytes
if platform == "tiktok":
return len(text.encode("utf-16-le")) // 2 # TikTok counts UTF-16 units
return len(text)
def main(run):
run = Path(run)
banned = [ln.strip().lower() for ln in Path("rules/banned.txt").read_text().splitlines()
if ln.strip() and not ln.startswith("#")]
fails = []
s = json.loads((run / "script.json").read_text())
words = len(s["script"].split())
if not MIN_WORDS <= words <= MAX_WORDS:
fails.append(f"script is {words} words (want {MIN_WORDS}-{MAX_WORDS})")
texts = {"title": s["title"], "script": s["script"]}
cap_file = run / "captions.json"
if cap_file.exists():
caps = json.loads(cap_file.read_text())
for plat, fields in caps.items():
for key, val in fields.items():
texts[f"{plat}.{key}"] = val if isinstance(val, str) else " ".join(val)
for plat, limit in LIMITS.items():
text = caps.get(plat, {}).get("caption", "")
if size(plat, text) > limit:
fails.append(f"{plat} caption is over {limit}")
for plat in NO_LINKS:
if URL.search(caps.get(plat, {}).get("caption", "")):
fails.append(f"{plat} caption has a link (not clickable there)")
for where, text in texts.items():
for phrase in banned:
if re.search(r"(?<!\w)" + re.escape(phrase) + r"(?!\w)", text.lower()):
fails.append(f'{where}: banned phrase "{phrase}"')
if fails:
print("FAIL")
for f in fails:
print(" -", f)
sys.exit(1)
print(f"PASS ({words} words" + (", captions checked)" if cap_file.exists() else ")"))
if __name__ == "__main__":
if len(sys.argv) != 2:
sys.exit(__doc__)
main(sys.argv[1])engine/avatar.py · lines 1-13 of 54. Copy all copies the whole file; read it in full on the page.
#!/usr/bin/env python3
"""Script -> your avatar speaking it (HeyGen, Avatar IV, 9:16, 1080p), through the HeyGen CLI.
python3 engine/avatar.py runs/<name> --dry-run # print the request, spend nothing
python3 engine/avatar.py runs/<name> # render (spends HeyGen credits) -> runs/<name>/avatar.mp4
Reads HEYGEN_AVATAR_ID and HEYGEN_VOICE_ID from the environment or .env. The CLI handles login.
"""
import json
import os
import subprocess
import sys
from pathlib import Path#!/usr/bin/env python3
"""Script -> your avatar speaking it (HeyGen, Avatar IV, 9:16, 1080p), through the HeyGen CLI.
python3 engine/avatar.py runs/<name> --dry-run # print the request, spend nothing
python3 engine/avatar.py runs/<name> # render (spends HeyGen credits) -> runs/<name>/avatar.mp4
Reads HEYGEN_AVATAR_ID and HEYGEN_VOICE_ID from the environment or .env. The CLI handles login.
"""
import json
import os
import subprocess
import sys
from pathlib import Path
def env(name):
if os.environ.get(name):
return os.environ[name]
if Path(".env").exists():
for line in Path(".env").read_text().splitlines():
key, _, val = line.partition("=")
if key.strip() == name and val.strip():
return val.strip().strip('"')
sys.exit(f"{name} is not set: add it to .env")
def main(run, dry):
run = Path(run)
s = json.loads((run / "script.json").read_text())
req = {"type": "avatar", "avatar_id": env("HEYGEN_AVATAR_ID"), "voice_id": env("HEYGEN_VOICE_ID"),
"script": s["script"], "aspect_ratio": "9:16", "resolution": "1080p",
"engine": {"type": "avatar_iv"}, "title": s["title"][:60]}
print(json.dumps(req, indent=2))
if dry:
print("(dry run: nothing rendered, no credits spent)")
return
r = subprocess.run(["heygen", "video", "create", "-d", json.dumps(req), "--wait"], capture_output=True, text=True)
if r.returncode:
sys.exit(f"heygen video create failed (exit {r.returncode}): {(r.stderr or r.stdout)[:800]}")
data = json.loads(r.stdout).get("data") or {}
vid = data.get("video_id") or data.get("id")
if not vid or data.get("status") == "failed":
sys.exit(f"render failed: {r.stdout[:800]}")
out = run / "avatar.mp4"
r = subprocess.run(["heygen", "video", "download", vid, "--output-path", str(out)], capture_output=True, text=True)
if r.returncode or not out.exists():
sys.exit(f"rendered {vid} but the download failed: {(r.stderr or r.stdout)[:500]}")
print(f"saved {out} (HeyGen video {vid})")
if __name__ == "__main__":
if len(sys.argv) < 2:
sys.exit(__doc__)
main(sys.argv[1], "--dry-run" in sys.argv)engine/post.py · lines 1-15 of 98. Copy all copies the whole file; read it in full on the page.
#!/usr/bin/env python3
"""Schedule a finished video on YouTube, Facebook, Instagram, TikTok and Threads through Upload-Post.
python3 engine/post.py runs/<name> --at 2026-10-06T12:15 # dry run: print every job
python3 engine/post.py runs/<name> --at 2026-10-06T12:15 --send # really schedule it
(add --tz Europe/London if you're not on US Eastern; leave out --at to post right away)
Needs runs/<name>/final.mp4 and captions.json, and a PASS from engine/check.py. Reads UPLOAD_POST_API_KEY,
UPLOAD_POST_PROFILE and (optional) FACEBOOK_PAGE_ID from the environment or .env. AI disclosure is on for
every job. The key is never printed.
"""
import argparse
import json
import os
import subprocess#!/usr/bin/env python3
"""Schedule a finished video on YouTube, Facebook, Instagram, TikTok and Threads through Upload-Post.
python3 engine/post.py runs/<name> --at 2026-10-06T12:15 # dry run: print every job
python3 engine/post.py runs/<name> --at 2026-10-06T12:15 --send # really schedule it
(add --tz Europe/London if you're not on US Eastern; leave out --at to post right away)
Needs runs/<name>/final.mp4 and captions.json, and a PASS from engine/check.py. Reads UPLOAD_POST_API_KEY,
UPLOAD_POST_PROFILE and (optional) FACEBOOK_PAGE_ID from the environment or .env. AI disclosure is on for
every job. The key is never printed.
"""
import argparse
import json
import os
import subprocess
import sys
from pathlib import Path
ENDPOINT = "https://api.upload-post.com/api/upload"
def env(name, required=True):
if os.environ.get(name):
return os.environ[name]
if Path(".env").exists():
for line in Path(".env").read_text().splitlines():
key, _, val = line.partition("=")
if key.strip() == name and val.strip():
return val.strip().strip('"')
if required:
sys.exit(f"{name} is not set: add it to .env")
return None
def jobs(c, at, tz):
"""One upload per platform group, each with its own caption as the job's title."""
common = [("user", env("UPLOAD_POST_PROFILE")), ("is_ai_generated", "true"), ("async_upload", "true")]
if at:
common += [("scheduled_date", at), ("timezone", tz)]
yt, fb = c["youtube"], c["facebook"]
ytfb = [("platform[]", "youtube"), ("platform[]", "facebook"), ("title", yt["title"]),
("youtube_title", yt["title"]), ("youtube_description", yt["description"]),
("selfDeclaredMadeForKids", "false"),
("facebook_title", fb["title"]), ("facebook_description", fb["description"]),
("facebook_is_ai_generated", "true")]
ytfb += [("tags[]", t) for t in yt.get("tags", [])]
page = env("FACEBOOK_PAGE_ID", required=False)
if page:
ytfb.append(("facebook_page_id", page))
ig = c["instagram"]["caption"]
tt = c["tiktok"]["caption"]
th = c["threads"]["caption"]
return [("youtube+facebook", common + ytfb),
("instagram", common + [("platform[]", "instagram"), ("title", ig), ("instagram_title", ig),
("media_type", "REELS")]),
("tiktok", common + [("platform[]", "tiktok"), ("title", tt), ("tiktok_title", tt)]),
("threads", common + [("platform[]", "threads"), ("title", th), ("threads_title", th)])]
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("run")
ap.add_argument("--at", help="local date and time, e.g. 2026-10-06T12:15")
ap.add_argument("--tz", default="America/New_York")
ap.add_argument("--send", action="store_true", help="really upload (default: dry run)")
a = ap.parse_args()
run = Path(a.run)
if subprocess.run([sys.executable, "engine/check.py", str(run)]).returncode:
sys.exit("fix the check failures first")
video = run / "final.mp4"
if not video.exists():
sys.exit(f"no {video} yet")
at = a.at + ":00" if a.at and len(a.at) == 16 else a.at
js = jobs(json.loads((run / "captions.json").read_text()), at, a.tz)
for label, fields in js:
print(f"\n[{label}]")
for k, v in fields:
print(f" {k} = {v if len(v) < 100 else v[:97] + '...'}")
if not a.send:
print("\n(dry run: nothing sent; add --send to schedule)")
return
key = env("UPLOAD_POST_API_KEY")
sent = {}
for label, fields in js:
cmd = ["curl", "-sS", "-X", "POST", ENDPOINT, "-H", f"Authorization: Apikey {key}",
"-F", f"video=@{video}", "-w", "\n%{http_code}"]
for k, v in fields:
cmd += ["--form-string", f"{k}={v}"] # literal text: a caption starting with @ is not a file
r = subprocess.run(cmd, capture_output=True, text=True)
body, _, code = r.stdout.rpartition("\n")
print(f"[{label}] HTTP {code}: {body[:300]}")
sent[label] = {"http": code, "response": body[:2000]}
(run / "sent.json").write_text(json.dumps(sent, indent=1))
print(f"\nsaved the responses (job and request ids) in {run / 'sent.json'}")
if __name__ == "__main__":
main()These live in .env in your engine folder. .gitignore keeps it out of git. Never paste it into a chat, a doc or a screen recording.
UPLOAD_POST_API_KEYLets post.py schedule through your Upload-Post account.
Upload-Post → API Keys → Generate New API KeyUPLOAD_POST_PROFILEThe profile name you created (the API's user). Not a secret, but keeping it here keeps the scripts generic.
FACEBOOK_PAGE_IDOptional. Which Facebook Page gets the post, if your login manages more than one.
The Facebook Pages call in setup step 7HEYGEN_AVATAR_IDYour photo avatar's look id.
heygen avatar looks listHEYGEN_VOICE_IDYour cloned voice's id.
heygen voice listHEYGEN_API_KEYOnly if you use the API-key route instead of OAuth. It bills the separate API balance. Set it in your shell or save it with heygen auth login --api-key, not in .env (avatar.py doesn't pass .env to the CLI).
ANTHROPIC_API_KEYOnly if you run Claude Code on Console API billing instead of a Pro/Max login. Export it in your shell; Claude Code asks once to approve it.
The Claude Console (platform.claude.com)First, make sure Claude actually reads your rules. Run this from inside your engine folder:
Test 1: does Claude read your rules?
claude -p "Read CLAUDE.md and the rule files it imports. In three short bullets, tell me the rules you'll follow most closely when you write my scripts, then list every banned phrase from rules/banned.txt." --permission-mode dontAsk --allowedTools "Read"A good answer quotes your rules back: your title, your 60-110 word range, your actual banned phrases. When this guide was tested with the starter files above, it also pointed out the placeholders still unfilled and two phrases the rules forbid that weren't on the banned list yet. That's the kind of answer you want.
A bad answer is generic advice about “engaging content.” That means your files didn't load: check that CLAUDE.md is in the folder you ran the command from, and that its @rules/... lines match the file names exactly.
Test 2: the free half of the chain · Part 1 of 2 · Copy copies all 10 lines
mkdir -p runs/test
echo "I used to answer every new lead myself. Now an assistant drafts the first reply and I call back the ones that need me." > runs/test/thought.txt
claude -p "Follow prompts/01-script.md. Run folder: runs/test" --permission-mode acceptEdits --allowedTools "Read,Write"
cat runs/test/script.json
python3 engine/check.py runs/test
python3 engine/avatar.py runs/test --dry-run
npx hyperframes doctormkdir -p runs/test
echo "I used to answer every new lead myself. Now an assistant drafts the first reply and I call back the ones that need me." > runs/test/thought.txt
claude -p "Follow prompts/01-script.md. Run folder: runs/test" --permission-mode acceptEdits --allowedTools "Read,Write"
cat runs/test/script.json
python3 engine/check.py runs/test
python3 engine/avatar.py runs/test --dry-run
npx hyperframes doctor
set -a; source .env; set +a
curl -s -H "Authorization: Apikey $UPLOAD_POST_API_KEY" https://api.upload-post.com/api/uploadposts/meTest 2: the free half of the chain · Part 2 of 2 · Copy copies all 10 lines
set -a; source .env; set +a
curl -s -H "Authorization: Apikey $UPLOAD_POST_API_KEY" https://api.upload-post.com/api/uploadposts/meWhat good looks like: script.json has a title, a script that sounds like you, and a notes list explaining what Claude changed; check.py prints PASS (… words); the dry run prints the HeyGen request with your two ids, "engine": {"type": "avatar_iv"}, 9:16 and 1080p, and says no credits were spent; doctor finds Node.js, FFmpeg and Chrome; Upload-Post answers with your email and plan. The script step took about a minute when this guide was tested.
If check.py prints FAIL, that's the guardrail doing its job. Read the reasons, fix the thought (or the rules, if they're wrong for you), and run the script step again.
The real thing, in the order I'd run it, with the thought from the episode. Swap in yours. Only two steps spend money (the avatar render and the post), and both wait for you first.
One thought, in your words, no polish. Numbers only if you can back them up. Here's the one from the episode:
Make the run folder and the thought
mkdir -p runs/2026-10-06-retest
cat > runs/2026-10-06-retest/thought.txt <<'EOF'
For a while my AI avatar just wasn't worth it. A cloned
voice over my screen recordings was cheaper and honestly
looked better, so I said no. Then the avatars got good
enough, and I switched. This isn't really about avatars.
Pick the cheapest thing that clears the bar today, and put
a date on the calendar to re-test whatever you said no to.
EOFmkdir -p runs/2026-10-06-retest
cat > runs/2026-10-06-retest/thought.txt <<'EOF'
For a while my AI avatar just wasn't worth it. A cloned
voice over my screen recordings was cheaper and honestly
looked better, so I said no. Then the avatars got good
enough, and I switched. This isn't really about avatars.
Pick the cheapest thing that clears the bar today, and put
a date on the calendar to re-test whatever you said no to.
EOFacceptEdits lets Claude write files without stopping to ask, and the only tools it gets are Read and Write, so apart from basic file commands it can't run anything in this step.
Headless Claude writes script.json
claude -p "Follow prompts/01-script.md. Run folder: runs/2026-10-06-retest" --permission-mode acceptEdits --allowedTools "Read,Write"Read it out loud. If it doesn't sound like you, edit script.json by hand, or rerun step 2 with a note on the end (“shorter, less formal”). This is approval gate one, and it's yours.
Read and check
cat runs/2026-10-06-retest/script.json
python3 engine/check.py runs/2026-10-06-retestThe dry run shows exactly what goes to HeyGen. The real run waits for the render and downloads avatar.mp4. Avatar IV is named in every request instead of left to HeyGen's default, so a default changing on their side can't quietly swap the engine on you.
Dry run, then the real render
python3 engine/avatar.py runs/2026-10-06-retest --dry-run
python3 engine/avatar.py runs/2026-10-06-retestThis makes a 1080×1920 HyperFrames project with your clip already placed, and transcribes the audio with Whisper into transcript.json, all on your own machine. Nothing is designed yet; that's the next step. The first run can take a while because it sets up Whisper (about 12 minutes on the Mac this guide was tested on). A hyperframes check on this bare project can fail on purpose (there's no animation yet), so don't panic.
Scaffold the HyperFrames project
npx hyperframes init runs/2026-10-06-retest/edit --video runs/2026-10-06-retest/avatar.mp4 --resolution portrait --non-interactiveClaude uses the HyperFrames skills to add captions, a title card and a graphic or two, runs npx hyperframes check until it's clean, then renders final.mp4. This is the slow step. The allowed tools let it edit files and run HyperFrames, ffprobe and ffmpeg, and nothing else that changes your machine.
Headless Claude + HyperFrames
claude -p "Using /hyperframes:hyperframes, follow prompts/02-edit.md. Run folder: runs/2026-10-06-retest" --permission-mode acceptEdits --allowedTools "Read,Write,Edit,Skill,Bash(npx hyperframes *),Bash(ffprobe *),Bash(ffmpeg *)"With sound, start to finish, on your phone if you can. Do the captions match the words? Is anything covering your mouth? Is the AI label there? If not, fix it in plain English (there's a prompt for that under Real use) and render again. Approval gate two.
Open the video
open runs/2026-10-06-retest/final.mp4 # macOS; on Linux: xdg-openClaude writes one caption per platform, then runs the guardrail check itself and fixes the captions until it passes. Run the check once more yourself; trust, but verify.
Captions, then check
claude -p "Follow prompts/03-captions.md. Run folder: runs/2026-10-06-retest" --permission-mode acceptEdits --allowedTools "Read,Write,Edit,Bash(python3 engine/check.py *)"
python3 engine/check.py runs/2026-10-06-retestThe dry run prints every job it would send. Read it, then add --send. The time is read in the --tz you give it (US Eastern by default), and it has to be in the future and no more than 365 days out.
Why four jobs instead of one? In my own setup on 2026-09-27, a scheduled job carrying a separate caption for Instagram and TikTok showed only the shared title on those two. So the script sends Instagram, TikTok and Threads as their own jobs, each with its caption as the title. YouTube and Facebook go together because their own fields worked. Every job carries is_ai_generated=true, and the Facebook job adds facebook_is_ai_generated (more on why below).
Dry run, then schedule
python3 engine/post.py runs/2026-10-06-retest --at 2026-10-06T12:15
python3 engine/post.py runs/2026-10-06-retest --at 2026-10-06T12:15 --sendSee or cancel what's scheduled
set -a; source .env; set +a
curl -s -H "Authorization: Apikey $UPLOAD_POST_API_KEY" "https://api.upload-post.com/api/uploadposts/schedule?profile_username=$UPLOAD_POST_PROFILE"
curl -s -X DELETE -H "Authorization: Apikey $UPLOAD_POST_API_KEY" https://api.upload-post.com/api/uploadposts/schedule/<job_id>set -a; source .env; set +a
curl -s -H "Authorization: Apikey $UPLOAD_POST_API_KEY" "https://api.upload-post.com/api/uploadposts/schedule?profile_username=$UPLOAD_POST_PROFILE"
curl -s -X DELETE -H "Authorization: Apikey $UPLOAD_POST_API_KEY" https://api.upload-post.com/api/uploadposts/schedule/<job_id>Your viewers are watching an AI avatar, so say so. Upload-Post maps one flag, is_ai_generated, to each platform's own label where the platform has one. It doesn't check whether you set it; per its docs, labeling accurately is the publisher's responsibility. That's you. Here's what each platform gets:
is_aigcis_ai_generatedcontainsSyntheticMediafacebook_is_ai_generated (Reels)Start claude inside your engine folder and paste one of these. Swap <name> for the run folder. Claude will ask before running commands, which is fine: you're right there.
1 · A week of thoughts from your notes
Read notes/this-week.md. Pull out five thoughts I could make a 30-second video about. For each one: a single sentence in my words, the one idea, and why someone would stop scrolling for it. No numbers I didn't write. Don't write scripts yet.2 · Tighten a script that runs long
Rewrite runs/<name>/script.json to 70-85 words. Keep my first sentence and my last sentence exactly as they are. Cut the middle, not the point. Update word_count and add a note saying what you cut. Then run python3 engine/check.py runs/<name> and fix anything it flags.3 · Fix one thing in the edit
Using /hyperframes:hyperframes: in runs/<name>/edit the captions sit too low and cover my chin. Move every caption up so it clears my face, change nothing else, run npx hyperframes check runs/<name>/edit, then render again to runs/<name>/final.mp4.4 · A quick critic before you watch
Use ffmpeg to save a frame from runs/<name>/final.mp4 at 0.5 seconds and then every 3 seconds into runs/<name>/frames/. Look at each frame and list anything wrong: text too small to read on a phone, a caption touching my face, a graphic that doesn't match what I'm saying at that moment, a frame that's mostly empty, a missing AI avatar label. One line each: time, problem, fix. Don't change any files.Use ffmpeg to save a frame from runs/<name>/final.mp4 at 0.5 seconds and then every 3 seconds into runs/<name>/frames/. Look at each frame and list anything wrong: text too small to read on a phone, a caption touching my face, a graphic that doesn't match what I'm saying at that moment, a frame that's mostly empty, a missing AI avatar label. One line each: time, problem, fix. Don't change any files.5 · Captions that start a conversation
Rewrite the threads and instagram captions in runs/<name>/captions.json so each ends with a question someone could answer in one line, using my words from the script. Follow CLAUDE.md, no links. Then run python3 engine/check.py runs/<name> and fix anything it flags.Number 4 is a baby version of my critic. It's surprisingly good at spotting the caption sitting on your chin before your audience does.
List prices on 2026-10-04, not a quote. Prices change; check the pages linked at the bottom.
Billed yearly, Claude Pro is $17 a month ($200 a year) and Upload-Post Basic $16 a month ($192 a year). HeyGen bills yearly too; its page has the number.
What HeyGen's credits buy: HeyGen lists an Avatar IV photo look at 16 credits per minute of video. So 600 credits is about 37 minutes of avatar video a month, if every credit goes to that. A short is under a minute, but the same credits also pay for new looks (one credit each) and anything else you make in HeyGen. Unused plan credits roll over for one extra billing cycle.
Claude usage: every headless run counts toward your plan's limits (a rolling five-hour window plus a weekly cap on paid plans). One video's worth of runs is small, but a big batch can hit the cap; Claude's pricing page covers waiting, upgrading or paying for extra usage.
Partly. HyperFrames is free. Upload-Post's free plan covers 10 uploads a month without TikTok. HeyGen's free plan lists 3 videos a month up to a minute, but voice cloning is listed from Creator up. Claude Code needs at least Pro. And if you'd rather pay HeyGen per render than monthly, the API-key route bills a separate pay-as-you-go balance with no plan; check the rate on its API page before a batch.
Honest part. What you just built is one clean slice. Mine is the same slice with a pile of guardrails and a feedback loop on top, and those took weeks of getting it wrong first. Here's what's in it, so you know what you're not getting from a diagram:
Start with the slice. Add a critic the first time a video makes you wince.
claude: command not found right after installingOpen a new terminal window. Still missing? The install folder isn't on your PATH yet; Claude Code's troubleshooting page has the fix for your shell. claude doctor shows install health.
claude -p run says it isn't allowed to do somethingHeadless runs can't stop and ask you. Add that exact tool to --allowedTools (for example Bash(npx hyperframes *); the space before the * matters), or do that step yourself.
Run Test 1 again. If Claude can't quote your rules, CLAUDE.md isn't loading (wrong folder, or the @rules/ paths don't match). If it can, VOICE.md needs more before→after examples of how you actually talk.
check.py says FAILGood, that's its job. Fix the script or caption it names, or loosen rules/banned.txt and the word range in check.py if they're wrong for you, then run it again.
That's auth or permission. Run heygen auth status, then log in again with heygen auth login --oauth. If HEYGEN_API_KEY is set in your shell, it overrides the saved login: unset it or check it's the right key.
A timeout: the --wait window ran out (20 minutes by default, per the CLI readme). The render may still finish. Check it with heygen video get <video-id> and download it with heygen video download <video-id> --output-path runs/<name>/avatar.mp4.
Use a photo where your eyes, mouth and lips are clearly visible and your face takes up a good part of the frame (HeyGen's photo avatar guide).
npx hyperframes init can't transcribe the clipRun npx hyperframes doctor; it reports whisper-cpp. Or run init again with --skip-transcribe and let the edit prompt build the captions from script.json.
hyperframes check reports errorsErrors have to be fixed before a render; each one names the element and usually a fix hint. Paste the output into an interactive claude session in your engine folder and ask it to fix exactly those.
Each render worker is its own Chrome (about 256 MB each, per the CLI docs). Add --workers 1, or --quality draft while you're iterating.
The key is wrong or missing. Check UPLOAD_POST_API_KEY in .env (no quotes, no spaces) with the /api/uploadposts/me call from setup step 7.
Either the free plan's 10 uploads a month are used up, or you hit a daily cap for one account (per rolling 24 hours: Instagram 50, TikTok 15, YouTube 10, Facebook 25, Threads 50). The reply names the cap.
TikTok posting is on Upload-Post's paid plans only.
Switch it to a Business or Creator account. The Instagram API doesn't support personal accounts.
Set FACEBOOK_PAGE_ID from the Facebook Pages call in setup step 7.
In Manage Users, disconnect that account, connect it again, and approve every permission it asks for. Changing that network's password breaks the connection too.
A temporary block on Instagram's side, usually 24 to 48 hours; Upload-Post pauses feed posts for that account for 24 hours. Clear any review prompt in the Instagram app, then post less often with varied captions. Retrying can make it last longer.
doctor checks all three).~/.claude/projects/ (30 days by default). In these commands it can only use the tools you list.telemetry command for its anonymous usage data.~/.heygen/credentials (that's where the login lives). If you used an API key, remove it from your shell too, and manage it from the API dashboard where you made it (app.heygen.com/developers/api)./logout in a session. claude plugin uninstall hyperframes@hyperframes removes the plugin. To remove Claude Code itself (native install): rm -f ~/.local/bin/claude and rm -rf ~/.local/share/claude.What each page backs up is listed at the end of the full guide.
One thought in, one labeled, scheduled video out, and you approve the script and the cut before anything goes anywhere. Build it, break it, then add the pieces from my version one at a time.
Want help setting this up? Receipts Group builds these systems.
Open the full guide as a page (every file in full, plus the table of contents).