Build an AI Video Studio, Part 2: A Claude Code Skill That Writes Filmable Scripts

Cover Image for Build an AI Video Studio, Part 2: A Claude Code Skill That Writes Filmable Scripts
Video AI5 min read

AI Video Studio with Claude Code + Remotion

🛠 Build it: 1. Setup & your first skill · 2. The script-writing skill · 3. The editing skill · 4. The local AI story skill · 5. The comedy script skill

🎬 Use it: Talking-head video · AI story video · Deadpan comedy video

🔬 Under the hood: Architecture · Subtitles · Jump cuts · Music & SFX · Local AI pipeline

In Part 1 you built hello-video: a skill where Claude writes a small JSON file, a script validates it, and Remotion renders it. This part builds the first real skill of the studio, tiktok-script. You give it a proverb, a quote or an opinion; it gives you back a 30–45 second talking-head script that's actually good to film, plus a shooting guide you can tape next to your camera.

Writing skills are where most people's Claude Code automations fall apart. Ask an AI for "a TikTok script about X" and you get something generic: a hook that starts with "Hey guys", a story with no specifics, a moral delivered like a school essay. The fix isn't a better one-line prompt. It's giving Claude the craft knowledge a good short-form writer has, a strict output contract, and a validator that refuses to let weak structure through. That's what this part builds.

What the skill produces

Every run creates a folder content/<YYYY-MM-DD>-<HH>-<MM>-<slug>/ with three files:

FileRead byWritten by
script.jsonMachines — the editing skill in Part 3 consumes itClaude
script.mdYou — the script as a tablegenerated
shoot.mdYou, at the camera — setup checklist, filming order, cue cardsgenerated

Claude writes only the JSON. Both Markdown files are generated from it by a script, every time. If Claude wrote both, they would drift apart within two edits; generating them makes that impossible.

The finished skill has this layout:

.claude/skills/tiktok-script/
├── SKILL.md                    ← the procedure
├── references/
│   ├── framework.md            ← the craft: hooks, timing, story rules, anti-patterns
│   └── output-format.md        ← the script.json contract, field by field
├── scripts/
│   ├── new-dir.mjs             ← same idea as Part 1
│   ├── validate-script.mjs     ← schema + craft rules, zero dependencies
│   └── build-docs.mjs          ← script.json → script.md + shoot.md
└── assets/example/
    ├── script.json             ← one complete, valid example
    ├── script.md
    └── shoot.md

Build it in the order below: contract first, craft second, procedure last. The procedure is easy to write once you know exactly what it must produce.

Step 1: Design the contract — script.json

A short talking-head video in this style has four blocks, and the contract encodes them as roles:

RoleShare of the videoJob
hook~8% (≤ 4 s)Stop the scroll. Explain nothing.
story~45%One concrete, real-life situation viewers recognize themselves in
punchline~30%Say the quote word for word, then twist it into a new angle
cta~17% (4–10 s)A specific question people can answer in one comment

A scene in the contract looks like this — a real one, from the skill's bundled example (the visual field is a camera direction: "switch to eye level, gesture lightly toward the back of the monitor"):

{
  "id": "s2",
  "role": "story",
  "startSec": 3,
  "endSec": 12,
  "visual": "Chuyển góc quay ngang tầm mắt, đưa tay chỉ nhẹ ra sau màn hình máy tính.",
  "voiceover": "Nói thật với anh em, bản chất tôi lành tính lắm. Nhưng mà nhiều khi nhìn mấy pha xử lý đi vào lòng đất... không nói ra thì nó ngứa mồm không chịu nổi.",
  "caption": {
    "text": "Bản chất lành tính... nhưng đời không cho phép!",
    "highlight": ["lành tính"],
    "style": "normal",
    "emoji": null
  },
  "cueWords": ["bản chất lành tính", "pha xử lý đi vào lòng đất", "không nói ra thì ngứa mồm"],
  "sfx": ["sigh"],
  "bRoll": null
}

Each field exists because something downstream needs it:

  • voiceover is the exact text that will be said. The editing skill uses it as the spelling of the subtitles, so it must be correct.
  • caption is not a transcript. It's a headline for the block (at most 60 characters ideally, 90 hard limit) so people watching muted still follow. highlight lists words to color, and each must appear verbatim in caption.text.
  • cueWords are memory prompts: three to five short phrases taken from the speaker's own lines, in order, meant to be glanced at next to the lens — not a teleprompter.
  • startSec/endSec are targets for filming, not truth. The real timeline comes from the footage in Part 3.

Top-level fields carry the quote, title, duration (20–60 s, ideally 30–45), fps (30), size (1080×1920), music choice and volume, five to eight hashtags and a post caption. The full field-by-field spec goes into references/output-format.md. Write that file now, as a table per object. It's the document Claude consults while writing, and the one you'll consult when the validator complains.

Step 2: Write the craft reference — framework.md

This file is the difference between a generic script and a good one. It's long — around 200 lines — and Claude reads it at one specific step, before writing a single word. Organize it into sections Claude can apply mechanically:

Tone. Write like you're on a video call with a close friend, not giving a TED talk. Sentences under 15 words. Strong verbs and concrete images. Self-deprecating humor that mocks situations, never people. Then list what's banned: preachy openers ("you should…", "did you know that…"), empty morals with no example, marketing phrases ("secret", "shocking truth", "99% of people don't know").

The time budget. The four-block table above, with the hard constraints: the hook is at most 4 s, the CTA 4–10 s, 4–6 scenes in total, first scene hook, last scene cta, exactly one punchline.

A hook library. Eight named patterns, each with a template. Claude picks three, drafts one hook per pattern, and keeps the sharpest:

PatternTemplate
Subverted expectationRead half the quote seriously, then swerve
Shocking confessionAdmit to doing what you once mocked
A question that stingsAsk about a pain everyone shares
A bare number"3 a.m. Bug number 14. It was a semicolon."
Someone else's wordsQuote the classic line: "Just a quick one, it's simple"
Helpless reactionA sigh, a head shake, then the first line
Flat denial"This saying is wrong. At least for people with jobs."
Mid-sentence start"…and that was the third time this week."

Story rules. One situation, explored deeply, beats three shallow ones. Every story needs at least one line of direct speech from another person, in quotes, and at least one concrete detail — a number, a time, a tool, a file name. The viewer must recognize themselves before second 12.

Punchline techniques. Five ways to twist a saying (flip the second half, add a condition, change who it's about, bring it down to office language, elevate something trivial into a principle), and one ban: never explain the saying like a dictionary.

CTA patterns with the rule that "What do you think?" is a dead CTA. The question must be answerable in one comment.

Anti-patterns, as a checklist Claude rereads before saving the file.

Writing this file is where your taste goes. Spend real time on it: every hour you put into framework.md improves every script the skill will ever write.

Step 3: The validator — encode the craft rules

validate-script.mjs checks the schema, and — more importantly — the craft rules that are easy to break while being creative. It's plain Node with no dependencies, collects every problem before reporting, and exits 1 if there's any ERROR:

const errors = [];
const warns = [];
const err = (m) => errors.push(m);
const warn = (m) => warns.push(m);
// ...checks...
for (const m of errors) console.log(`ERROR  ${m}`);
for (const m of warns) console.log(`WARN   ${m}`);
process.exit(errors.length > 0 ? 1 : 0);

The checks worth copying:

// Scenes must be contiguous: no gaps, no overlaps, and they fill the whole duration.
if (i === 0 && s.startSec !== 0) err(`${at}: the first scene must start at 0`);
if (i > 0 && s.startSec !== scenes[i - 1].endSec) err(`${at}: startSec must equal the previous endSec`);
if (i === scenes.length - 1 && s.endSec !== doc.durationSec) err(`${at}: the last scene must end at durationSec`);

// Structure.
if (s.role === 'hook' && dur > 4) err(`${at}: hook is ${dur}s, max 4s`);
if (s.role === 'cta' && (dur < 4 || dur > 10)) warn(`${at}: cta is ${dur}s, aim for 4–10s`);

// Speaking pace: words per second, the most common way a script fails on camera.
const wps = s.voiceover.trim().split(/\s+/).length / dur;
if (wps > 7.0) err(`${at}: ${wps.toFixed(1)} words/s — too dense, cut words or lengthen the scene`);
else if (wps > 6.5) warn(`${at}: ${wps.toFixed(1)} words/s — a bit fast (sweet spot 4.0–6.5)`);
else if (wps < 3.0) warn(`${at}: ${wps.toFixed(1)} words/s — too sparse, the scene will drag`);

// Every highlight must exist verbatim, or the renderer has nothing to color.
c.highlight.forEach((h) => {
  if (!c.text.includes(h)) err(`${at}: highlight "${h}" does not appear verbatim in caption.text`);
});

// The quote must actually be said in the punchline.
if (!norm(punch.voiceover).includes(norm(doc.quote))) {
  warn('the punchline voiceover does not contain the quote word for word');
}

Also check that id equals the folder name — Part 3 builds footage paths from it — and that every scene has 2–6 cueWords with at least one of them actually taken from the voiceover.

The split between ERROR and WARN is a design decision. ERRORs are things that break the pipeline or the format; WARNs are craft judgments where a deliberate exception is fine — a 4.5-second hook that needs a pause, say. The skill tells Claude to fix WARNs or explain to the user why it kept them.

Step 4: The generator — build-docs.mjs

This script reads script.json and writes both Markdown files. Two details make the shooting guide genuinely useful rather than a reformatted copy of the JSON.

Film the hook last. By the time you've told the story, you're warmed up and the opening comes out natural. The generator computes the filming order from the roles:

// Film every scene first, the hook LAST — by then you're warmed up.
const order = [...doc.scenes.keys()].filter((i) => doc.scenes[i].role !== 'hook');
const hookIdx = doc.scenes.findIndex((s) => s.role === 'hook');
if (hookIdx !== -1) order.push(hookIdx);

Pacing advice per scene, computed from words per second, so the speaker knows whether to rush or breathe:

const wps = (words(s.voiceover) / dur).toFixed(1);
const pace = wps >= 6 ? "fast, don't pause much"
           : wps <= 4 ? "relaxed, room to breathe"
           : "medium, natural pauses";

Each scene in shoot.md becomes a card: camera and acting notes, the cue words joined with arrows, the lines with a reminder to understand them, not memorize them, the pace, take checkboxes, and the exact file name to save — raw/s2.mov. That file-naming instruction is the contract with Part 3, and putting it on every card is how you make sure it's followed.

The guide also opens with a setup checklist written for someone holding a phone, not a cinematographer: phone vertical and fixed, lens at or slightly above eye level, 50–70 cm away, 4K if available (the editor zooms in slightly at cuts), beauty filter off, exposure locked, window light in front, lapel mic below the collarbone, airplane mode on, and a five-second test recording before the real thing.

script.md is the simpler output: a metadata table and the four-column script table (time, visual, voiceover, on-screen text with highlights in bold). Both files start with an HTML comment saying they're generated and must not be edited.

Step 5: One complete example

Put one complete, validated script.json in assets/example/, and run build-docs.mjs on it so the example Markdown files exist too. Claude uses the example to understand the format; the files also serve as your regression test when you change the generator.

There's a trap here: Claude imitates examples closely. If the example is about office life with a coworker who says "it works on my machine", you'll get an endless stream of office stories with that exact line. So the skill says, in its hard rules: the example is a format reference, not a content bank — never reuse its situations. The framework's story bank also says "suggestions, never reuse verbatim."

Step 6: The procedure — SKILL.md

Now the skill itself. The real one is written in Vietnamese, the language of its user; write yours in the language you work in, since Claude follows either equally well. Here is its structure in English:

---
name: tiktok-script
description: Create a 9:16 selfie-style TikTok script (30–45 s) from a proverb, quote,
  opinion or topic. Writes content/<date>-<time>-<slug>/script.json (the data contract
  for the editing pipeline) plus script.md and shoot.md. Use when the user says "write a
  script", "make a video about this quote", or pastes a quote and wants a short video.
---

# TikTok Selfie Script

Role: a short-form video strategist — an IT person with life experience, casual, witty.

## Steps

### 1. Extract the input
quote (required), context (default "a desk with a computer"), durationSec (default 38),
angle (default "IT / office worker").
- Given a topic instead of a quote → coin a short quote yourself and SAY SO in the reply.
- Given several quotes → pick the sharpest one. Don't merge.
- Quote longer than 25 words → shorten it to something sayable in 4 seconds.
Don't ask trivial questions.

### 2. Read references/framework.md — mandatory, before writing anything.

### 3. Brainstorm internally (do NOT print)
- 3 hooks from 3 different patterns → keep the sharpest.
- 1 concrete situation with a number/time/tool and one line of direct speech.
- 1 twist on the quote.

### 4. Write the 4 blocks: Hook → Story → Punchline & Quote → CTA.
4.0–6.5 words/second. The caption is a headline, not a transcript.

### 4b. Write cueWords for each scene (3–5 phrases from the lines, in order).

### 5. Create the folder: node .claude/skills/tiktok-script/scripts/new-dir.mjs <slug>
Never type the date or time yourself.

### 6. Write script.json. id = the exact folder name. Spec: references/output-format.md.
Write ONLY script.json.

### 7. Validate, then generate docs (mandatory)
node .../validate-script.mjs content/<dir>/script.json && \
node .../build-docs.mjs content/<dir>/script.json
ERROR → fix and re-run. WARN → fix, or explain why you kept it.

### 8. Reply
The script table, the three file paths (say that shoot.md is the one to open when
filming), the validation result in one line, and any quote you coined or shortened.
Do not paste the JSON.

## Hard rules
- Everyday Vietnamese. "tôi"/"mình", address viewers as "anh em"/"mọi người".
- No preaching. Self-deprecating humor. Never insult jobs, regions, genders or looks.
- Never invent statistics, studies or quotes from real people.
- Never copy assets/example/ content — it's a format reference only.

A few choices in this procedure are worth stealing for any writing skill:

  • "Brainstorm internally, do not print." Drafting three hooks and keeping the best one noticeably improves the result, but printing all three makes the reply noisy. Claude can do the comparison without showing it.
  • "Say so if you invented something." When Claude has to coin a quote or shorten yours, the reply must say so. Silent liberties erode trust in a skill faster than anything else.
  • Defaults instead of questions. Every optional input has a default, and the skill says "don't ask trivial questions." A writing skill that interrogates you before writing is one you'll stop using.
  • The reply format is specified. A table, paths, one validation line. No JSON dump.

Step 7: Test it

/tiktok-script "Có công mài sắt, có ngày nên kim"

Watch what happens: Claude reads the framework, creates content/…-co-cong-mai-sat/, writes the JSON, and runs the validator. It's common to see a first run fail on pacing — a 9-second story scene with 70 words — and Claude cut the lines and re-run without being asked. The reply is a script table and a pointer to shoot.md.

Then test the edges:

TryWhat should happen
A topic instead of a quote: /tiktok-script deadlineClaude coins a quote and says it did
A 40-word quoteIt's shortened, and the reply says so
"Make it 25 seconds"durationSec 25 → the validator WARNs (sweet spot 30–45), Claude keeps it and explains
Edit voiceover by hand to 100 words, run the validator yourselfERROR on words per second

Open shoot.md. If you'd actually be comfortable filming from it — clear order, readable cue cards, the file name for each scene — the skill is done. If something feels off, fix the generator or the framework, never the generated file.

What you've learned

  • Contract first. Design the JSON around what downstream code needs, field by field.
  • Craft lives in a reference file read at a specific step, not crammed into the prompt.
  • Validators enforce taste, not just syntax: pacing, structure, verbatim highlights.
  • Generate every human-readable file from the JSON.
  • Examples are dangerous: label them "format only."

The script is ready and the clips are filmed. Part 3 builds the skill that turns them into a finished video: a six-stage pipeline with silence detection, word-level speech timing, and a Remotion composition with animated subtitles.