Part one got you from one idea to one finished video. This part is about doing it two weeks at a time. You build a library of scripts from your own work, teach it to sound like you, plan two weeks so your feed doesn't look like one video posted fourteen times, make the batch, and schedule all of it through Upload-Post in one sitting.
It builds on the folder from part one's guide. Haven't built that yet? Start there. Same as last time, you don't need to know code. The easy way: paste one prompt into Claude Code, and Claude builds the new pieces with you, asking what it needs as it goes.
The easy way: let Claude build it with you
Same deal as part one. You paste one prompt into Claude Code, and Claude turns into your setup helper. It interviews you about your work, writes the scripts with you, and builds the new pieces right into your part-one folder. Your job is talking about what you know and saying yes or no. Claude does the typing.
What you need
- Part one, working. The folder from part one's guide (
~/content-engine), with at least one video made start to finish. Skipped it? The setup prompt sends you back there first. Sorry. It's worth it. - Your own material. Notes, how-tos, checklists, emails where you explained something, old posts, a talk you gave. Your scripts get built from this. Leave out anything with a customer's name or private details.
- Some time to talk. Claude interviews you: 50 questions in five rounds, and you can stop between rounds.
- HeyGen credits for the batch. Every video is one avatar render. On the Creator plan from part one, two weeks of short videos fits in a month's credits if you don't spend them on much else. Check your balance before you render.
- Optional: a few looks. Extra HeyGen looks of you (another outfit, another room), so the feed doesn't look like the same video every day. One look works too.
No new accounts. It's the same starter stack as part one: about $73 a month at list prices. Prices change, so check each page before you buy.
Open Claude Code in your part-one folder
Open the terminal (part one's guide shows how) and paste these two lines. If Claude asks whether you trust the folder, say yes.
cd ~/content-engine
claudePaste in the setup prompt
Click 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.
Hi Claude. Please be my patient setup helper again.
Last time we built my content engine in this folder: one idea in, one finished, scheduled video out. Now I want two weeks of videos at a time. A library of scripts built from my own work, in my voice, planned two weeks at a time, made as one batch and scheduled in one sitting. I don't know how to code, so treat me like a smart beginner.
THE GUIDE
We're following this guide:
https://receiptsgroup.com/guides/two-weeks-of-videos
Its "manual way" sections are the source of truth for every file and command. 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/plan.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. Part one's guide is https://receiptsgroup.com/guides/ai-content-engine if you need it.
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.
- 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. Never show what's inside .env.
2. Ask me first before anything that costs money or credits, and before anything that posts in public. Do the free dry run first, every time.
3. Three steps wait for me to type an answer: the avatar renders (python3 engine/batch.py avatar), watching the videos (python3 engine/batch.py watch) and the real schedule (python3 engine/schedule.py --send). Never run those yourself. Tell me the exact command, I run it in a second terminal window, and you help me read what it says. The edit and captions steps run for a long time, so I run those there too.
4. AI disclosure stays on. Every video and every caption says it was made with an AI avatar.
5. My avatar is only me. Extra looks are new looks of me, never anyone else.
6. Scripts come only from my material and my answers. No made-up stories, numbers, results, customers or quotes. Leave out customer names and private details, even if they're in my files.
7. A script goes into a batch only after I've said ok to it. A video gets scheduled only after I've watched it and said yes.
8. Stay in this folder. Ask before you install anything, and tell me what it is in plain words.
THE PLAN
Go one step at a time. After each step, run any free check 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. Check part one.
Look for CLAUDE.md, rules/VOICE.md, rules/GUARDRAILS.md, prompts/01-script.md to 03-captions.md, and engine/check.py, avatar.py and post.py. Run python3 engine/check.py on one of my runs/ folders. If part one isn't here or doesn't work, stop and help me finish part one first.
Step 2. Add the new pieces. (Guide: "Set up the new pieces".)
Make the sources, library and plans folders. Add the new line to CLAUDE.md. Copy rules/SHAPE.md, prompts/04-topics.md to 08-voice-pass.md, and engine/plan.py, batch.py and schedule.py from the guide. Show me the list of what you added.
Step 3. Gather my material. (Guide: "Build your script library from your own work", steps 1 and 2.)
Ask me what I've got: notes, how-tos, checklists, emails where I explained something, old posts, talks. Help me copy them into sources/ as text files. Remind me to leave out anything with a customer's name or private details. Then follow prompts/04-topics.md and show me the topics by series. I cut the ones that aren't me and add any you missed.
Step 4. Interview me. (Guide: "Build your script library from your own work", step 3.)
Follow prompts/05-interview.md: 50 questions in five rounds of ten, one at a time. I can stop between rounds and pick it up another day.
Step 5. My voice. (Guide: "Make it sound like you".)
Show me rules/VOICE.md and help me write my note at the top: how I want to sound, in my words. Then follow prompts/06-scripts.md for the first 10 topics, and prompts/07-review.md to go through them with me, one at a time. Every line I fix goes into my voice guide as a before-and-after pair. Once there are at least five pairs, follow prompts/08-voice-pass.md so the rest of the drafts match.
Step 6. Fill the library.
Repeat: 06 for the next 10, then 07 with me, then 08 when I've fixed a few more lines. Keep going until I've said ok to at least 14 scripts from at least 3 series. Ask if I want to stop for today between rounds.
Step 7. My looks. (Guide: "Plan two weeks", step 1.)
Ask if I have more than one HeyGen look of me. If I do, help me find their ids with heygen avatar looks list and put them in looks.txt. If not, skip it. One look works fine.
Step 8. The plan. (Guide: "Plan two weeks", step 2.)
Ask me which day to start (a few days out, so there's time to make everything) and what time I want to post. Run engine/plan.py. Show me the two weeks and ask if it looks right.
Step 9. The batch. (Guide: "Make the batch".)
Run python3 engine/batch.py to show me where everything is. Then walk me through the steps in my second terminal window: avatar (it spends HeyGen credits, so remind me first), edit, captions. They take a while. Tell me I can walk away, and how to pick up if one stops.
Step 10. Watch. (Guide: "Make the batch", step 5.)
I run python3 engine/batch.py watch and watch every video with sound, start to finish. Help me fix any I say no to.
Step 11. Schedule. (Guide: "Schedule the whole batch".)
Run python3 engine/schedule.py (the dry run). Explain anything that isn't ready, in plain words. When every day says ready, show me one day's full dry run with --show. Then I run --send myself. The AI label stays on.
Step 12. Wrap up.
Give me a short cheat sheet for the next batch: the commands in order, and the three things I always do myself (say ok to every script, watch every video, say yes before anything is scheduled).
Ready? Say hi and ask me your first question.What to expect
- It checks part one first. If something's missing, you fix that before anything new gets built.
- Then your material. Claude asks what you've written down and helps you put it in one folder. Then it turns it into a list of topics, and you cut the ones that aren't you.
- Then the interview. One question at a time, about your story, how you work and what you think. Answer like you're talking to a friend. Stories beat polish. Typos are fine.
- Then the part that makes it sound like you. You read drafts out loud and say ok, cut, or how you'd actually say it. Every fix goes into your voice guide, and the rest of the drafts get rewritten to match.
- Then two weeks, planned, made and scheduled. The steps that spend credits, the watching and the real scheduling wait for you to type yes in your own terminal window. Claude tells you the command; you run it.
- Nothing costs money or goes public without your yes. Same as part one. Every time.
How long: the library is the long part, an afternoon or two, and you can split it across days. After that, a batch is: plan it, let the computer make it, then watch and schedule the whole thing in one sitting.
What you'll have at the end: a library of scripts from your own work, a voice guide full of your own before-and-after examples, and two weeks of videos you've watched, approved and scheduled, with the AI label on.
Need a break? 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.
cd ~/content-engine
claude --continueWant to see every step yourself? The manual way is below.
What you're adding
This is the manual way: 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.
Part one was a straight line: one thought, one video. This puts a library in front of it and a batch around it. Each box is one job, and each one leaves a file behind, so when something comes out weird you can see which step did it.
- 1Your materialYou: notes, how-tos, emails, old posts→ sources/
- 2The topicsClaude Code reads your material→ library/topics.md
- 3The interviewClaude asks 50 questions, you answer→ library/interview.md
- 4The draftsClaude Code, in your voice and the six-beat shape→ library/scripts.json
- 5Your reviewYou, out loud: ok, cut, or how you'd say it→ approved scripts and a better rules/VOICE.md
- 6The planengine/plan.py: one a day, no series twice in a row, looks take turns→ plan.json and a run folder per day
- 7The batchengine/batch.py: part one's steps for every day→ final.mp4 and captions.json per day
- 8Your yesYou watch every video→ approved.txt per day
- 9The scheduleengine/schedule.py, through Upload-Post, AI label on→ two weeks queued on every app
Words you'll see
- Script library: every script you could post, in one file, each marked draft, approved or cut.
- Series: a group of topics, like pricing or hiring. The planner uses them so the same kind of video doesn't run two days in a row.
- Voice guide:
rules/VOICE.md. A page of real examples: the stiff version, and how you'd actually say it. - Look: one HeyGen avatar of you in a different outfit or room.
- Batch: a set of videos made together. Here, two weeks' worth.
- Slot: the day and time a video is scheduled to post.
- Dry run: a practice run. It shows what would happen and does nothing.
Before you start
No new tools. This runs on part one's starter stack (Claude Code, HeyGen, HyperFrames and Upload-Post), about $73 a month at list prices. What's new is a few files and a lot more of you.
You need
- Part one, working. One video made start to finish, so you know the chain works before you ask it to do fourteen.
- Your material, in plain text or Markdown files. Anything else, paste into a text file first.
- HeyGen credits. One avatar render per video. Check your balance before the avatar step.
- Room in your Claude plan. The edit step runs Claude once per video, and every headless run counts toward your plan's usage limits. If you hit a limit partway, the batch stops, and you run the same step again later. It picks up where it stopped.
Time
The library is the long part: an afternoon or two, and most of it is you talking about your work. After that, a batch is three moves: plan it (a second), let the computer make it (that part runs while you do something else), then watch and schedule it all in one sitting. You do the thinking once. The computer does the waiting.
What you'll have at the end
- A script library built from your own work, every script marked draft, approved or cut.
- A voice guide full of your own before-and-after examples. Every new script follows it, part one's single videos too.
- Two weeks of finished videos, one a day, each one watched and approved by you.
- All of it scheduled on YouTube, Facebook, Instagram, TikTok and Threads, labeled as AI-generated wherever the app supports it.
- Three commands you'll run again every two weeks.
Set up the new pieces
Everything goes in your part-one folder: one new rule file, five prompts, three small scripts and a one-line change to CLAUDE.md. Here's where it all lands:
content-engine/
├── CLAUDE.md now also loads rules/SHAPE.md
├── rules/
│ ├── VOICE.md how you talk, plus your before-and-after pairs
│ ├── GUARDRAILS.md what never goes out (part one)
│ ├── SHAPE.md the six beats every script follows (new)
│ └── banned.txt (part one)
├── prompts/
│ ├── 01 to 03 part one: script, edit, captions
│ ├── 04-topics.md your material -> topics (new)
│ ├── 05-interview.md 50 questions -> more topics (new)
│ ├── 06-scripts.md topics -> draft scripts (new)
│ ├── 07-review.md you review the drafts; fixes go in VOICE.md (new)
│ └── 08-voice-pass.md rewrite the drafts to match VOICE.md (new)
├── engine/
│ ├── check.py, avatar.py, post.py part one
│ ├── plan.py two weeks from your approved scripts (new)
│ ├── batch.py part one's steps for every day (new)
│ └── schedule.py the whole batch to Upload-Post (new)
├── sources/ your material (new)
├── library/
│ ├── topics.md every topic, by series
│ ├── interview.md your answers, in your words
│ └── scripts.json the library: draft, approved or cut
├── looks.txt optional: your HeyGen look ids
├── plan.json the batch you're working on
├── plans/ finished batches
└── runs/
└── 2026-10-19-P01/ one folder per day, same as part onecd ~/content-engine
mkdir -p sources library plansThen add one line to CLAUDE.md, so the six-beat shape rides along on every Claude run, part one's single videos included:
# 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
@rules/SHAPE.md
Work only inside this folder. Never open, print or copy .env.Every other new file sits in the section where you use it, in a code box labeled with its file name. Save each one as you go. None of them touch your accounts. If you keep this folder in git, add sources/ to .gitignore too: it's your private material.
Build your script library from your own work
Here's the thing about a content calendar you download: it's somebody else's calendar. My library came from my real work. I had AI dig through the systems I've built and the notes I keep, and that turned into 119 topics. Then I answered 50 interview questions about my story and how I work, and that added 20 more. 139 scripts, and every one of them traces back to something I actually did or think.
You'll do the same thing, smaller. Your files first, then an interview for everything you never wrote down.
Put your material in sources/
Copy in anything where you explained your work in your own words: notes, how-tos, checklists, emails where you answered a question, old posts, the outline of a talk. More is better, and messy is fine. Plain text or Markdown works best.
Leave out anything with a customer's name or details, prices you charged someone, passwords or keys, and anything you'd mind seeing on a screen.
ls sources/PermissionsNone. Claude Code sends what it reads to Anthropic to work on it (part one's guide covers what each tool can see), which is one more reason to leave customer details out.Turn it into topics
Claude reads everything in
sources/and writes a topic list, grouped into a handful of series, each topic with the file it came from and a short quote. Then you read the list and cut anything that isn't really you.Find video topics in my own material: every file in sources/ (my notes, how-tos, emails I wrote, old posts, anything I made). 1. Read every file in sources/. If library/topics.md exists, read it too. 2. Pull out the ideas a short video could teach. For each one write: - the idea in one line, in my words - its series: 4-6 short series names that fit my work (reuse them, don't make a new one for every topic) - where it came from: the file name and a short quote from it 3. Rules: - Only ideas that are really in my material. No invented stories, results, numbers, customers or quotes. - Skip anything with a customer's name or details, prices I charge someone, passwords, or anything marked private. - Two topics that teach the same lesson count as one. 4. Write library/topics.md as a numbered list under a heading per series. If it already exists, add the new topics at the end and don't repeat old ones. Change no other file. End with one line: DONE <number of new topics> or FAILED <reason>.claude -p "Follow prompts/04-topics.md" --permission-mode acceptEdits --allowedTools "Read,Write,Edit" cat library/topics.mdLet Claude interview you
Your files only cover what you wrote down. The interview gets the rest: your story, how you actually work, what people ask you, what you think your field gets wrong, and what you never want said. It's a conversation, so run it in a normal Claude session, not with
-p. Answer like you're talking to a friend. Stop between rounds whenever you want; paste the same line next time and it starts at the next question.Interview me so my scripts come from my real story and sound like me. Ask me 50 questions in five rounds of ten, ONE question at a time. Wait for my answer before the next one. Short, plain questions. Follow up when an answer has a story in it. Round 1: my story. How I got into this work, the turns, what I learned the hard way. Round 2: how I actually do the work, step by step, and the tools I use. Round 3: who I work with, what they ask me most, what they get wrong. Round 4: my opinions. What my field overcomplicates, oversells or gets backwards. Round 5: my lines. What I never talk about, words I'd never say, how I want to come across. After each round: - Save my answers in my exact words to library/interview.md (question, then answer). - Add every new idea to library/topics.md with its series and the source "interview, question <number>". - Add phrases I use a lot to a "Phrases I use" list at the end of rules/VOICE.md. - Ask if I want to keep going or stop for today. If I stop, the next time I paste this prompt, read library/interview.md and start at the next question. Never invent an answer or put words in my mouth. Change no other file.cd ~/content-engine claude # then type: Follow prompts/05-interview.mdWrite the first ten drafts
Each run writes the next ten topics into
library/scripts.jsonas drafts. Don't write all of them yet. Write ten, review them (next section), fix your voice guide, then write the rest. Otherwise you fix the same stiff line a hundred times.Write scripts for my library from library/topics.md. 1. Read library/topics.md, library/interview.md (if it's there) and library/scripts.json (if it's there). 2. Take the next 10 topics that don't have a script yet. 3. For each one, write the script I'll say as my avatar. Follow CLAUDE.md and the rules it imports: my voice, my guardrails and the six-beat shape. 60-110 words. Use my own words from the source and the interview wherever they work. No numbers, names or results that aren't in my material. 4. Add each one to library/scripts.json (a JSON list; make it if it's missing): {"id": "letters and numbers only, e.g. P07", "series": "the topic's series", "title": "the hook as a title, 60 characters max", "script": "the full script as plain text", "source": "the topic's source line", "status": "draft", "notes": ["anything you weren't sure about"]} Don't change the scripts that are already there. Change no other file. End with one line: DONE <the new ids> or FAILED <reason>.claude -p "Follow prompts/06-scripts.md" --permission-mode acceptEdits --allowedTools "Read,Write,Edit"
Make it sound like you
My first drafts had a problem. They didn't sound like me. They sounded like a training manual. So I told the AI what I wanted: more casual and funny, and not so proper. That note is now the first rule in my voice guide.
But a note only gets you so far. “Casual” means something different to everybody. What actually works is examples. Every fix went into the guide: the stiff version, and how I'd actually say it, side by side. Then every script got rewritten to match. Here's one pair from mine:
Before: “The decision starts with the missing capability, not with how many subscriptions I can cancel.”
How I'd actually say it: “Could I spend my time making my own chat app? Probably. Do I need another project? Absolutely not.”
Same point. One sounds like a memo, the other sounds like me. Your guide needs your pairs, not mine.
Put your note at the top of your voice guide
Open
rules/VOICE.mdfrom part one. Under the first line, write how you want to sound, in one or two lines, in your words. Keep everything else that's there. Its “Before -> how I'd actually say it” list is where your pairs go, and part one's two example pairs can go once you have real ones.# How I talk My note: <how you want to sound, in your own words. Mine is "more casual and funny, and not so proper."> Who's talking: <your job title> at <your company> ...Review the drafts out loud
Read each draft out loud. Your ear catches what your eyes skip. Say ok, cut, or how you'd say it. Every fix goes into your voice guide as a pair, in your exact words. That's the step that makes it sound like you, and it's the one you can't hand off.
Help me review my draft scripts in library/scripts.json, one at a time. For each script with "status": "draft": 1. Show me its title and script. Ask me to read it out loud. 2. Wait for my answer: - "ok": set its status to "approved". - "cut": set its status to "cut". - anything else is a fix: rewrite it my way and show me again. 3. Every time I fix a line, add the pair to the "Before -> how I'd actually say it" list in rules/VOICE.md: your line, then my line, in my exact words. 4. After every five scripts, ask if I want to keep going. Never approve a script I didn't say ok to. Change nothing else.cd ~/content-engine claude # then type: Follow prompts/07-review.mdRewrite the rest to match
After a review round with five or more new pairs, have Claude rewrite every draft that's still waiting. Then write the next ten (prompt 06), review them (07), and go again. The goal is fewer fixes every round.
My voice guide (rules/VOICE.md) has new before-and-after examples. Rewrite every script in library/scripts.json with "status": "draft" so it sounds like them. - Keep each script's idea, example and facts. Change how it sounds, not what it says. - Keep the six-beat shape (rules/SHAPE.md) and 60-110 words. - Leave "approved" and "cut" scripts exactly as they are. - Add a note to each rewritten script saying what you changed. Change no other file. End with one line: DONE <number rewritten> or FAILED <reason>.claude -p "Follow prompts/08-voice-pass.md" --permission-mode acceptEdits --allowedTools "Read,Write,Edit"
Shape every script like the top videos
Next, I wanted to know what actually works in my space. So we studied 98 short videos from 14 channels: each channel's top videos against its typical ones. Three things stood out:
| What the top videos did | Top | Typical |
|---|---|---|
| Got to the first usable idea | 8.3 seconds in | 11.8 seconds in |
| Had a surprise or turn around the middle | 56% | 4% |
| Ended on a punchline | 19% | 0% |
So every script gets rebuilt in one shape: six beats. Here they are with the real lines from one of my videos, a short about testing your landing page on your own phone:
| Beat | What it does | From my video |
|---|---|---|
| 1 · The hook | The point, a question or a result. No warm-up. | “Perfect speed score. Still can't find the phone number.” |
| 2 · The idea | The one thing to do, said plainly | Grab your own phone and do the one job the page is for. |
| 3 · An example | One situation you can picture | A plumbing page. The phone number's at the very bottom. |
| 4 · A surprise | The turn around the middle | Heatmaps can help. They're also not psychic. |
| 5 · Something to copy | Words, a check or a step to use today | Check three things: the offer, an answer, the form. |
| 6 · The punchline | The last line pays off the hook | “Couldn't find your number, but wow, that speed score.” |
# The shape of every script
Six beats, in this order. A beat can be one short sentence.
1. The hook. The point, a question someone asked me, or a result, in the
first sentence. No warm-up, no "in this video".
2. The idea. The one thing to do or know, said plainly, in the first few seconds.
3. An example. One real situation from my work, with a detail you can picture.
4. A surprise. A turn around the middle: the catch, the part people get
wrong, or a dry aside.
5. Something to copy. Exact words, a check or a step the viewer can use today.
6. The punchline. The last line pays off the hook. Don't trail off. Don't pitch.
- One idea per video. Two ideas means two videos.
- 60-110 words (engine/check.py checks).
- Mix up the hooks. Two scripts in a row never open the same way.CLAUDE.md loads it (see “Set up the new pieces”), so the drafts, the voice pass and part one's single scripts all follow it.
Plan two weeks
Now a planner lays out the two weeks. It follows two rules: the same series never runs two days in a row, and your looks take turns, so your feed never looks like the same video over and over. It takes a second and spends nothing.
Optional: list your looks
A look is another HeyGen avatar of you: another outfit, another room. I rotate six. You don't need any extra: with one look (or no
looks.txt), the planner skips this and every video uses your part-one avatar. Want more? Make new looks from your own photo in HeyGen (part one's guide covers it), then list their ids. Only you. Never anyone else's face.heygen avatar looks list # JSON: copy the id of each look of you# One HeyGen look id per line. Anything after a # is a note. <look id> # studio <look id> # office <look id> # patioRun the planner
It only takes scripts you've marked approved. Each day gets the next script from the series that has waited longest (never yesterday's), and the next look in your list. It makes a run folder per day with that day's
script.json, exactly like part one, and runscheck.pyon every one. A script that's already been used has a run folder, so it's skipped: the next batch never repeats this one.#!/usr/bin/env python3 """Plan a batch: one video a day from your approved scripts. The same series never runs two days in a row, and your looks take turns. Writes plan.json and one run folder per day (with its script.json), then runs the guardrail check on each day. python3 engine/plan.py --start 2026-10-19 # 14 days from that date, 12:15 each day python3 engine/plan.py --start 2026-10-19 --weekend-time 10:30 # a different time on Saturday and Sunday python3 engine/plan.py --start 2026-10-19 --days 7 --time 18:00 python3 engine/plan.py --start 2026-10-19 --redo # throw away a plan you haven't rendered yet Reads library/scripts.json (only scripts marked "approved") and looks.txt if you have one (one HeyGen look id per line; anything after a # is a note). A script that already has a run folder is skipped, so the next batch never repeats this one. Spends nothing and changes nothing outside plan.json and runs/. """ import argparse import datetime import json import shutil import subprocess import sys from pathlib import Path PLAN = Path("plan.json") def looks(): p = Path("looks.txt") if not p.exists(): return [] return [ln.split("#")[0].strip() for ln in p.read_text().splitlines() if ln.split("#")[0].strip()] def used_ids(): """Script ids that already have a run folder (runs/<date>-<id>).""" return {d.name.split("-", 3)[3] for d in Path("runs").glob("*-*-*-*") if d.is_dir()} def drop_old_plan(): """--redo: remove the old plan and its run folders, but only if nothing has been made in them yet.""" runs = [Path(d["run"]) for d in json.loads(PLAN.read_text())] made = [r.name for r in runs if r.exists() and {p.name for p in r.iterdir()} - {"script.json", "look.txt"}] if made: sys.exit("--redo only replaces a plan before anything is made. These days already have more than a " "script: " + ", ".join(made)) for r in runs: if r.exists(): shutil.rmtree(r) PLAN.unlink() def pick(todo, last_series, last_used): """The next script: never yesterday's series; among the rest, the series that has waited longest.""" options = [s for s in todo if s["series"] != last_series] or todo if options is todo and last_series is not None: print(f" note: only '{last_series}' scripts left, so it runs two days in a row") return min(options, key=lambda s: (last_used.get(s["series"], -1), todo.index(s))) def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--start", required=True, help="the first day, e.g. 2026-10-19") ap.add_argument("--days", type=int, default=14) ap.add_argument("--time", default="12:15", help="posting time, 24-hour clock, e.g. 12:15 or 18:00") ap.add_argument("--weekend-time", help="a different time on Saturday and Sunday") ap.add_argument("--redo", action="store_true", help="replace a plan you haven't rendered yet") a = ap.parse_args() if PLAN.exists(): if not a.redo: sys.exit("plan.json already exists. Finish that batch first, or add --redo to replace it.\n" "(After a batch is scheduled, engine/schedule.py moves its plan into plans/.)") drop_old_plan() lib = json.loads(Path("library/scripts.json").read_text()) used = used_ids() todo = [s for s in lib if s.get("status") == "approved" and s["id"] not in used] if len(todo) < a.days: sys.exit(f"only {len(todo)} approved scripts haven't been used yet; you asked for {a.days} days") start = datetime.date.fromisoformat(a.start) lk = looks() plan, last_series, last_used = [], None, {} for i in range(a.days): day = start + datetime.timedelta(days=i) s = pick(todo, last_series, last_used) todo.remove(s) last_series, last_used[s["series"]] = s["series"], i weekend = day.weekday() >= 5 at = f"{day.isoformat()}T{a.weekend_time if weekend and a.weekend_time else a.time}" run = Path("runs") / f"{day.isoformat()}-{s['id']}" run.mkdir(parents=True) (run / "script.json").write_text(json.dumps( {"title": s["title"], "script": s["script"], "word_count": len(s["script"].split()), "notes": [f"from library/scripts.json ({s['id']}, series {s['series']})"]}, indent=1)) entry = {"date": day.isoformat(), "at": at, "id": s["id"], "series": s["series"], "title": s["title"], "run": str(run)} if lk: entry["look"] = lk[i % len(lk)] (run / "look.txt").write_text(entry["look"] + "\n") plan.append(entry) PLAN.write_text(json.dumps(plan, indent=1)) fails = 0 for d in plan: r = subprocess.run([sys.executable, "engine/check.py", d["run"]], capture_output=True, text=True) result = r.stdout.strip().splitlines()[0] if r.stdout.strip() else "no answer" fails += r.returncode != 0 when = datetime.date.fromisoformat(d["date"]).strftime("%a %b %d") look = f" look {lk.index(d['look']) + 1}" if lk else "" print(f"{when} {d['at'][11:]} {d['series']:<12} {d['id']:<10} {result}{look}") if r.returncode: print(" " + "\n ".join(r.stdout.strip().splitlines()[1:])) print(f"\nplan.json: {len(plan)} days. " + (f"{fails} failed the check: fix those scripts, then run " "python3 engine/check.py on their run folders." if fails else "Every script passed the check.")) if __name__ == "__main__": main()python3 engine/plan.py --start 2026-10-19 --weekend-time 10:30Mon Oct 19 12:15 pricing P01 PASS (84 words) look 1 Tue Oct 20 12:15 hiring H01 PASS (97 words) look 2 Wed Oct 21 12:15 tools T01 PASS (72 words) look 3 Thu Oct 22 12:15 mistakes M01 PASS (88 words) look 1 Fri Oct 23 12:15 story S01 PASS (103 words) look 2 Sat Oct 24 10:30 pricing P02 PASS (79 words) look 3 Sun Oct 25 10:30 hiring H02 PASS (91 words) look 1 Mon Oct 26 12:15 tools T02 PASS (66 words) look 2 Tue Oct 27 12:15 mistakes M02 PASS (95 words) look 3 Wed Oct 28 12:15 story S02 PASS (82 words) look 1 Thu Oct 29 12:15 pricing P03 PASS (77 words) look 2 Fri Oct 30 12:15 hiring H03 PASS (90 words) look 3 Sat Oct 31 10:30 tools T03 PASS (70 words) look 1 Sun Nov 01 10:30 mistakes M03 PASS (86 words) look 2 plan.json: 14 days. Every script passed the check.Read it before you make anything
Does the order make sense? Does any week lean too hard on one series? Want a different start day or time? Run it again with
--redo. That only works before anything's rendered, on purpose. Leave a few days between today and the first post, so the batch has time to render.
Make the batch
Then it makes them, one after another. Same steps as part one: the video of you, the edit with captions and graphics, the captions for each app. The difference is that batch.py does one step for every day before the next step, and skips anything that's already done. So if it stops (your Claude limit, a HeyGen hiccup, your laptop going to sleep), you run the same command again and it picks up where it left off.
#!/usr/bin/env python3
"""Make every video in plan.json with part one's steps, one step at a time across the whole batch.
python3 engine/batch.py # where every day is (spends nothing)
python3 engine/batch.py avatar # render every missing avatar video (spends HeyGen credits; asks first)
python3 engine/batch.py edit # build and render every missing edit (Claude + HyperFrames; slow)
python3 engine/batch.py captions # write and check every missing set of captions
python3 engine/batch.py watch # play each finished video and ask you: post it or not?
Each step skips the days that already have what it makes, so you can stop any time and run it again.
A day with a look.txt (from engine/plan.py and your looks.txt) renders in that look.
"""
import json
import os
import shlex
import subprocess
import sys
from pathlib import Path
EDIT_TOOLS = "Read,Write,Edit,Skill,Bash(npx hyperframes *),Bash(ffprobe *),Bash(ffmpeg *)"
CAPTION_TOOLS = "Read,Write,Edit,Bash(python3 engine/check.py *)"
def ask(question):
"""Your typed answer, or None when nobody can type one (e.g. Claude running the step for you)."""
try:
return input(question).strip().lower()
except EOFError:
print("\nThis step needs your answer: run it yourself in a terminal window.")
return None
def days():
if not Path("plan.json").exists():
sys.exit("no plan.json yet: run engine/plan.py first")
return [Path(d["run"]) for d in json.loads(Path("plan.json").read_text())]
def run(cmd, env=None):
print(" $ " + " ".join(shlex.quote("python3" if c == sys.executable else c) for c in cmd), flush=True)
return subprocess.run(cmd, env=env).returncode == 0
def checks_ok(r):
return subprocess.run([sys.executable, "engine/check.py", str(r)], capture_output=True).returncode == 0
def status():
print(f"{'day':<26} {'check':<6} {'avatar':<7} {'edit':<5} {'captions':<9} you")
for r in days():
yes = lambda f: "yes" if (r / f).exists() else "-"
ok = (r / "approved.txt").read_text().strip() if (r / "approved.txt").exists() else "-"
print(f"{r.name:<26} {'PASS' if checks_ok(r) else 'FAIL':<6} {yes('avatar.mp4'):<7} {yes('final.mp4'):<5} "
f"{yes('captions.json'):<9} {ok}")
def avatar():
todo = [r for r in days() if not (r / "avatar.mp4").exists()]
bad = [r.name for r in todo if not checks_ok(r)]
if bad:
sys.exit("fix these scripts first (python3 engine/check.py runs/<day>): " + ", ".join(bad))
if not todo:
return print("every day already has its avatar video")
for r in todo:
print(f"{r.name}: {len(json.loads((r / 'script.json').read_text())['script'].split())} words")
if ask(f"\nRender {len(todo)} avatar videos with HeyGen? This spends credits. Type yes: ") != "yes":
return print("nothing rendered")
for r in todo:
env = dict(os.environ)
if (r / "look.txt").exists():
env["HEYGEN_AVATAR_ID"] = (r / "look.txt").read_text().strip()
if not run([sys.executable, "engine/avatar.py", str(r)], env):
sys.exit(f"stopped at {r.name}; run this step again to pick up from there")
def edit():
for r in days():
if (r / "final.mp4").exists() or not (r / "avatar.mp4").exists():
continue
print(f"\n{r.name}")
if not (r / "edit").exists() and not run(["npx", "hyperframes", "init", str(r / "edit"), "--video",
str(r / "avatar.mp4"), "--resolution", "portrait", "--non-interactive"]):
sys.exit(f"stopped at {r.name}: hyperframes init failed")
run(["claude", "-p", f"Using /hyperframes:hyperframes, follow prompts/02-edit.md. Run folder: {r}",
"--permission-mode", "acceptEdits", "--allowedTools", EDIT_TOOLS])
if not (r / "final.mp4").exists():
sys.exit(f"stopped at {r.name}: no final.mp4 (read Claude's last line above)")
def captions():
for r in days():
if (r / "captions.json").exists() or not (r / "final.mp4").exists():
continue
print(f"\n{r.name}")
run(["claude", "-p", f"Follow prompts/03-captions.md. Run folder: {r}",
"--permission-mode", "acceptEdits", "--allowedTools", CAPTION_TOOLS])
if not checks_ok(r):
sys.exit(f"stopped at {r.name}: the captions don't pass engine/check.py yet")
def watch():
opener = "open" if sys.platform == "darwin" else "xdg-open"
for r in days():
if not (r / "final.mp4").exists() or (r / "approved.txt").exists():
continue
print(f"\n{r.name}: {json.loads((r / 'script.json').read_text())['title']}")
try:
subprocess.run([opener, str(r / "final.mp4")])
except FileNotFoundError:
print(f" open {r / 'final.mp4'} yourself")
answer = ask(" Watched the whole thing, with sound? Post it? (yes / no / stop): ")
if answer in (None, "stop"):
break
if answer == "yes":
(r / "approved.txt").write_text("yes\n")
else:
print(" not approved: fix it (part one's Real use prompts help), then run watch again")
if __name__ == "__main__":
steps = {"status": status, "avatar": avatar, "edit": edit, "captions": captions, "watch": watch}
step = sys.argv[1] if len(sys.argv) > 1 else "status"
if step not in steps:
sys.exit(__doc__)
steps[step]()See where everything is
A table: every day, whether its script passes the check, and which steps are done. Free. Run it any time.
python3 engine/batch.pyRender the avatars (this one spends HeyGen credits)
It lists every day that still needs its avatar video and how long each script is, then waits for you to type yes. A day with a look renders in that look; the rest use your part-one avatar. Avatar IV is named in every request, same as part one.
python3 engine/batch.py avatarBuild the edits
The slow one. For each day, it starts the HyperFrames project and runs part one's edit prompt with the same allowed tools, so Claude adds the captions, the title and a graphic or two, checks the project and renders
final.mp4. Start it and go do something else.python3 engine/batch.py editWrite the captions
Part one's captions prompt, once per video, then the guardrail check. It stops on the first day whose captions don't pass, so nothing broken slips through.
python3 engine/batch.py captionsWatch every one. Then say yes.
It opens each finished video and asks: post it? Yes, no or stop. Watch the whole thing, with sound, on your phone if you can. Yes saves an
approved.txtin that day's folder, and nothing gets scheduled without one. No means fix it (part one's real-use prompts help) and run watch again. When the batch is done, I watch them and say yes. That part's not optional.python3 engine/batch.py watch
Schedule the whole batch
Now, the posting. Part one's post.py already sends one video to all five apps through Upload-Post, with its own caption for each app and the AI label on every app that has one. schedule.py runs it for every day in the plan, at that day's time.
Run the dry run first. It checks every day (the video's there, the captions pass the check, you said yes, the time is still in the future) and lists what it would schedule. Nothing gets sent.
#!/usr/bin/env python3
"""Schedule the whole batch in plan.json through Upload-Post, each video at its own day and time, with part
one's engine/post.py.
python3 engine/schedule.py # dry run: checks every day and lists what it would schedule
python3 engine/schedule.py --show # the same, plus every field post.py would send
python3 engine/schedule.py --send # really schedule them (asks first)
(add --tz Europe/London if you're not on US Eastern)
A day goes out only if it has final.mp4, captions that pass engine/check.py, your yes from
`engine/batch.py watch` (approved.txt) and a time that's still in the future. A day that already has
sent.json is skipped, so running --send again never posts anything twice. Once every day is sent,
plan.json moves to plans/ and you're ready to plan the next batch.
"""
import argparse
import datetime
import json
import subprocess
import sys
from pathlib import Path
try:
from zoneinfo import ZoneInfo
except ImportError: # Python 3.8 and older
ZoneInfo = None
def ask(question):
"""Your typed answer, or None when nobody can type one (e.g. Claude running the step for you)."""
try:
return input(question).strip().lower()
except EOFError:
print("\nThis step needs your answer: run it yourself in a terminal window.")
return None
def problems(day, tz):
r = Path(day["run"])
out = []
if (r / "sent.json").exists():
return ["already sent"]
for f, why in (("final.mp4", "no final.mp4"), ("captions.json", "no captions.json"),
("approved.txt", "you haven't said yes (engine/batch.py watch)")):
if not (r / f).exists():
out.append(why)
if (r / "captions.json").exists() and subprocess.run(
[sys.executable, "engine/check.py", str(r)], capture_output=True).returncode:
out.append("engine/check.py fails")
when = datetime.datetime.fromisoformat(day["at"])
now = datetime.datetime.now(ZoneInfo(tz)).replace(tzinfo=None) if ZoneInfo else datetime.datetime.now()
if when <= now:
out.append(f"{day['at']} is in the past: change its time in plan.json")
return out
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--tz", default="America/New_York")
ap.add_argument("--show", action="store_true", help="print every field post.py would send")
ap.add_argument("--send", action="store_true", help="really schedule (default: dry run)")
a = ap.parse_args()
plan_file = Path("plan.json")
if not plan_file.exists():
sys.exit("no plan.json: run engine/plan.py first")
plan = json.loads(plan_file.read_text())
ready = []
for d in plan:
p = problems(d, a.tz)
print(f"{d['at'].replace('T', ' ')} {d['id']:<10} {d['title'][:44]:<44} {'ready' if not p else '; '.join(p)}")
if not p:
ready.append(d)
sent = sum((Path(d["run"]) / "sent.json").exists() for d in plan)
print(f"\n{len(ready)} ready to schedule, {sent} already sent, {len(plan) - len(ready) - sent} not ready ({a.tz}).")
if a.show:
for d in ready:
print(f"\n=== {d['run']} at {d['at']}", flush=True)
subprocess.run([sys.executable, "engine/post.py", d["run"], "--at", d["at"], "--tz", a.tz])
if not a.send:
print("Dry run: nothing sent. Add --send to schedule the ready ones.")
return
if not ready:
return
if ask(f"Schedule {len(ready)} videos on every app in captions.json, AI label on? Type yes: ") != "yes":
return print("nothing sent")
for d in ready:
print(f"\n=== {d['run']} at {d['at']}", flush=True)
subprocess.run([sys.executable, "engine/post.py", d["run"], "--at", d["at"], "--tz", a.tz, "--send"])
sent_file = Path(d["run"]) / "sent.json"
if not sent_file.exists():
print(" not sent (read post.py's message above)")
continue
for label, res in json.loads(sent_file.read_text()).items():
if not str(res.get("http", "")).startswith("2"):
print(f" check {label}: HTTP {res.get('http')} (see {d['run']}/sent.json)")
if all((Path(d["run"]) / "sent.json").exists() for d in plan):
Path("plans").mkdir(exist_ok=True)
plan_file.rename(Path("plans") / f"plan-{plan[0]['date']}.json")
print(f"\nEvery day is sent. The plan moved to plans/plan-{plan[0]['date']}.json.")
if __name__ == "__main__":
main()The dry run
One line per day: ready, or exactly what's missing. Add
--showto see every fieldpost.pywould send for each day: the captions, the time, the time zone and the AI flags.python3 engine/schedule.py python3 engine/schedule.py --showSchedule it
It asks once, then schedules every ready day. Each day gets a
sent.jsonwith Upload-Post's replies, so running it again never sends a day twice. Once every day is sent,plan.jsonmoves intoplans/, and you're clear to plan the next batch. Times are read in US Eastern unless you add--tz, and Upload-Post takes dates up to a year out.python3 engine/schedule.py --sendCheck what's queued
The first call lists everything scheduled on your profile. Need to move a day? Cancel its jobs with the second call, one job id at a time (one per app group; the ids are in the list), then schedule that day again by hand with part one's
post.pyand a new time. Upload-Post can also change a scheduled job's date, title or caption; its scheduled-posts page has that call.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> python3 engine/post.py runs/<day> --at 2026-10-24T18:00 --send
And there it is. Two weeks, every slot filled. Approve it once, and you're done until the numbers come back.
Real use: prompts for real jobs
Start claude inside your engine folder and paste one of these. Claude will ask before it changes anything, which is fine: you're right there.
I added sources/this-week.md. Follow prompts/04-topics.md for that file only, then show me just the new topics. Don't write any scripts yet.For each script in library/scripts.json with "status": "draft", write a second hook of a different kind (a question someone asked me, a result, a mistake, a count). Show me both hooks side by side and ask which one to keep. Change a script only after I pick, and keep it 60-110 words.Read every approved script in library/scripts.json and my voice guide, rules/VOICE.md. List the ten lines that sound least like my before-and-after pairs: the script id, the line, and how I'd more likely say it. Don't change anything.Read the script.json in every run folder listed in plan.json. Tell me which hooks open the same way, which two scripts teach nearly the same thing, and which endings trail off instead of landing a punchline. One line each: the date, the problem, a fix. Don't change any files.Read the captions.json in every run folder listed in plan.json. Flag any two captions on the same app that open the same way or ask the same question. Rewrite the later one in my words from its script, then run python3 engine/check.py on that run folder and fix anything it flags.Number 4 is the one I'd never skip. Fourteen videos in a row is exactly where the sameness sneaks in.
What it costs
The same as part one: about $73 a month at list prices for the starter stack. Part one's guide has the breakdown by tool. Prices change, so check each page before you buy.
A batch doesn't add a tool. It uses more of what you already pay for:
- HeyGen credits: one avatar render per video. Check your balance before the avatar step.
- Claude usage: every headless run counts toward your plan's limits, and a batch is a lot of runs. If you hit a limit, the batch stops; run the same step again later.
- Upload-Post: one video a day stays far under its daily caps for each connected account (its limits page lists them). TikTok needs a paid plan, same as part one.
What my version adds (and this guide doesn't)
Honest part again. Here's what's in mine that isn't in this guide:
- A library page. All 139 scripts on one page, with search, a filter for each series and a copy button on every script.
- Two hooks per script. Every script in my library has two hook options written for it, so I can pick the stronger one.
- A stricter planner. Mine also lays out two weeks in under a second, never runs a series two days in a row and rotates six looks, plus a pile of rules about openings, layouts and repeats that took a while to get right.
- The critic on every video before it's scheduled (part one's guide covers it).
- One command for the whole schedule that checks every caption and lines each video up in its slot. The checks for a batch of 14 take a couple of seconds.
- The numbers coming back. Each post's numbers per app get pulled in, and the next plan tests one thing at a time.
- Comment GUIDE, the DMs and replies that probably sent you here.
Start with the plain version. Add the stricter rules the first time two videos in a row look like twins.
When it breaks
The topics are generic, or nothing like your work
Every script sounds the same
The drafts still sound stiff after the voice pass
A draft says something you never did
rules/banned.txt. If it's a habit, add a rule to rules/GUARDRAILS.md, like “Never mention a customer unless my material does.”plan.py: only N approved scripts haven't been used yet
--days.plan.py: only one series left, so it runs two days in a row
--redo.plan.py: plan.json already exists
schedule.py moves it to plans/ once every day is sent), or add --redo if nothing's been rendered yet.--redo says some days already have more than a script
A day says FAIL in the plan
runs/<day>/script.json, fix the script, and run python3 engine/check.py runs/<day>. Fix it in library/scripts.json too, so the library stays right.batch.py stops partway
“This step needs your answer: run it yourself in a terminal window”
cd ~/content-engine, and run that command yourself.schedule.py: a day's time is in the past
"at" in plan.json to a future time, same format (like 2026-10-24T18:00), then run it again.One app on one day came back with an error
The feed looks repetitive anyway
What each tool can see
The same as part one, plus your material: Claude Code sends what it reads from sources/ and your interview answers to Anthropic to work on them, and your library stays in this folder on your computer. Disconnecting works the same as part one, too.
That's two weeks
Your library, your voice, two weeks planned, made, watched and scheduled. Then you're done until the numbers come back. You do the thinking once. The computer does the waiting.
Part one: one idea to a finished, scheduled video, if you haven't built the engine yet. Next time: how the critic decides what's broken.
Checked on Oct 5, 2026 against
Every claim about a third-party tool in this guide (plans, prices, menu paths, commands, limits) was checked against these official pages on Oct 5, 2026. These screens change often: if something looks different, trust the page over this guide.
- Upload-Post: Upload video
scheduled_datemust be in the future and at most a year out;timezonesets how it's read; per-app titles fall back totitle;is_ai_generatedandfacebook_is_ai_generated. - Upload-Post: Manage scheduled postsList scheduled jobs by profile, cancel one, or change its date, time zone, title or caption.
- Upload-Post: Pricing and limitsDaily upload caps per connected account; TikTok posting on paid plans only; scheduling up to a year ahead.
- Upload-Post: Labeling AI-generated contentWhat
is_ai_generatedbecomes on each app,facebook_is_ai_generatedfor Reels, and “labeling accurately is your responsibility as the publisher.” - YouTube Help: Channel monetization policiesThe July 15, 2025 update renaming “repetitious content” to “inauthentic content,” clarified to include content that is repetitive or mass-produced.
- Claude Code: Run Claude Code programmatically
claude -p,--allowedToolsrule syntax,--permission-mode acceptEdits,--continue. - Claude Code: Memory (CLAUDE.md)CLAUDE.md loads at the start of every session;
@pathimports, relative to the file that imports them. - Claude: Plans and pricingWhich plans include Claude Code, and the usage limits a big batch can run into.
- HeyGen CLI on GitHub
heygen avatar looks list(checked against the CLI's own--help), video create and download. - HeyGen Help: How to use creditsWhat an Avatar IV render costs in credits per minute of video (a photo look costs less than a video look).
