story-video
Create narrated faceless story videos (mystery, drama, true-crime style fiction) with characters that keep the same face in every scene. Writes the script, generates character/location/prop reference
Files16
View on GitHubSKILL.mdsource/skill/story-video/SKILL.md
GitHubStory video
Turns an idea into a 3–5 minute vertical (9:16) narrated video built from AI illustrations.
Everything lives in a project folder with one story.json; scripts read it and write next to it.
my-episode/
story.json ← you write this (script + cast + scenes)
assets/ ← sheets + scene images (+ manifest.json)
audio/ ← one voice clip per line (+ manifest.json)
render/ ← timeline.json, audio.wav, resized frames
out/my-episode.mp4
0. First run: set yourself up (once)
Users often just say "clone https://github.com/huynhcongdac/story-video-skill and make me a video about …". If you were cloned like that, do the whole setup yourself before writing anything:
- Install where your agent finds skills (if you are not already there), so later sessions pick it up:
Claude Code
~/.claude/skills/story-video· Codex~/.agents/skills/story-video· Antigravity~/.gemini/antigravity/skills/story-video. Any other agent: keep the clone and work from it. - Check tools: Python 3.10+ (
pip install numpy pillow), Node.js 18+,ffmpegon PATH (or setFFMPEG_PATH). Install what is missing if you can, otherwise tell the user the exact command. - Create
.envfrom.env.example. Ask the user for one image key and one voice key — never require a specific provider:- Images:
IMAGE_PROVIDER=sangtao+SANGTAO_API_KEY, orIMAGE_PROVIDER=openai+OPENAI_API_KEY. - Voice:
TTS_PROVIDER=vbee+VBEE_APP_ID+VBEE_TOKEN, orTTS_PROVIDER=elevenlabs+ELEVENLABS_API_KEY+ELEVENLABS_VOICE_ID. Write the keys into.envyourself. Never print, echo or repeat keys back in chat.
- Images:
- Continue with step 1 below.
Choosing the image provider: both work. sangtao.ai is the cheapest option for this many images — one episode
needs 60 images, each scene carries up to 6 reference sheets (sangtao accepts 20 per image), and plans start from
about 200k VND / 500 images (7 episodes), pricing: https://sangtao.ai/vi/imagine?tab=pricing. API calls need a
plan or credit (no free tier on the API). Key: https://sangtao.ai · docs: https://sangtao.ai/vi/api-docs/chatgpt-image.
If the user already has an OpenAI key, that works too.
1. Write story.json
Ask the user for: topic/genre, language, length, and whether it is part of a series. Then write the file.
Start from examples/locked-room/story.json — copy its structure exactly. (It is a short ~3-minute
example with 41 lines; for a 4–5 minute episode write 45–55 lines.)
{
"title": "CĂN PHÒNG KHOÁ TRÁI", // shown on the title card
"series": "HỒ SƠ THÁM TỬ MINH", // optional red band above the title
"episode": 1,
"aspectRatio": "9:16",
"visualStyle": "Cinematic semi-realistic digital painting, moody film-noir lighting, …", // ONE style for every image
"labels": {}, // optional: override on-screen words (see "English videos" below)
"keywords": "độc|manh|mối|hung|thủ", // optional: words highlighted in captions (regex alternation)
"characters": {
"minh": {
"name": "Thám tử Minh",
"tag": "the detective in the beige trench coat", // short English visual tag, used inside scene prompts
"appearance": "Vietnamese man aged 40. BODY: 7.5-head proportion, … FACE: … HAIR: … OUTFIT: …",
"sheet": "https://…/sheet.png" // optional: reuse a sheet from an earlier episode
}
},
"locations": { "study": { "name": "…", "description": "English description of the place" } },
"props": { "watch": { "name": "…", "description": "English description of the object" } },
"keyframes": {
"k_watch": { "chars": [], "loc": ["study"], "props": ["watch"], "p": "English scene prompt …" }
},
"lines": [
{ "t": "Narration sentence in the video language.", "shots": [["k_clock", "shock"], ["k_body", "push"]], "sfx": ["chime"] },
{ "t": "Bạn đoán ai là hung thủ? …", "shots": [["k_lineup", "question"]], "countdown": 3 }
]
}
Characters — appearance always has four parts: BODY (with head-to-body proportion: 6 = short/stooped,
7 = average, 8 = tall), FACE, HAIR, OUTFIT, and starts with nationality + gender + age. Write it in English.
Shots — each line is split evenly across its shots. Aim for a new visual every 3–5 seconds (~1 shot per 8–12 words). Reusing a keyframe with a different effect is fine and saves images.
Effects (fx):
| fx | look | use for |
|---|---|---|
push / pan / slow |
Ken Burns zoom / lateral pan / gentle drift | normal narration |
punch |
zoom punch + white flash | a key detail |
shock |
punch + shake + RGB split | discoveries, doors bursting, screams |
glitch |
RGB split + slice jitter | "something is wrong" moments |
shake |
handheld tremble | fear, nervous characters |
flashback / flashpush / flashshock |
black & white + vignette | memories, the reveal reconstruction |
reveal |
slow push + shake + red pulse | the culprit's face |
title |
title card over the shot | right after the cold open |
suspect:N:NAME:role |
red suspect card | introducing each suspect |
clue:N:text |
yellow clue card | each clue (text may contain ":") |
question |
"who did it?" card (+ "countdown": 3 on the line) |
just before the reveal |
end |
follow / next episode card | last line |
sfx per line: chime thunder hit boom lock knock crash siren tick riser scream rain shimmer click.
Lines also accept pauseBefore / pauseAfter (seconds).
Writing rules (these are what make it watchable)
- Cold open in the first 3 seconds — start at the most shocking moment, then the title card. No intro.
- Open loop — state the question early (who, how) and don't answer it until the end.
- Fair clues — 3–4 clues, each shown on screen with a
cluecard and an image that clearly shows it. - Red herrings — 2–3 suspects with motive and a weakness; the culprit must have an alibi that the clues break.
- A hook every 45–60 s ("Nhưng có một chi tiết không ai để ý…").
- Question + countdown before the reveal, then a
revealshot and a fast black-and-white reconstruction. - Length — ~45–55 lines ≈ 4–5 minutes. Keep sentences short; one idea per line.
- Fiction only, no gore: show consequences (a body slumped, a cold cup), never wounds or blood.
Sensitive words are masked on screen automatically (
chết → ch*t); the voice is unchanged.
Image prompt rules
- Write
pin English, describe composition, camera, light and mood. Name characters exactly as incharacters[].name— the script appends theirtagso the model links the name to the right sheet. - List every character / location / prop that appears in the scene in
chars/loc/props: those sheets become the references. - Clue objects need physical descriptions, not facts: "the short hour hand just left of 12, the long minute hand slightly above the 9" works; "the watch shows 11:47" does not. Same for "cold porridge": describe the cracked skin, dry crust.
- No readable text in images; put words in cards instead.
- Flashbacks: start the prompt with "Black-and-white flashback:".
2. Generate images
python scripts/gen_assets.py my-episode sheets # ~10–15 sheets
python scripts/gen_assets.py my-episode keyframes # ~40–45 scene images
Then look at the results (open the PNGs, or build a contact sheet) and check: same face across scenes,
every clue clearly visible, no garbled text, flashbacks in black and white. Fix the prompt in story.json and
regenerate only the bad ones: python scripts/gen_assets.py my-episode keyframes k_watch --force.
3. Voiceover
python scripts/tts.py my-episode
Changing a line's text and re-running only redoes that line. ElevenLabs gives exact word timing; Vbee is estimated.
4. Build and render
python scripts/build_timeline.py my-episode
python scripts/render.py my-episode --still 30 900 3000 # check a few frames first
python scripts/render.py my-episode # → my-episode/out/my-episode.mp4
The first render runs npm install in remotion/. Look at the stills before the full render:
title fits on two lines, cards don't cover faces, captions readable.
Series
For episode 2+, set "sheet" on returning characters to the URL (or local path) of their sheet from episode 1
— they will look identical. With sangtao the sheet URLs are in the previous episode's assets/manifest.json.
English (or other language) videos
Write lines[].t in that language, pick a matching voice, and override labels:
"labels": {"suspect": "SUSPECT", "clue": "CLUE", "episode": "CASE", "question1": "WHO", "question2": "DID IT?", "comment": "Comment before you keep watching 👇", "next": "Next case?", "follow": "Follow for more"}.
Extend CENSOR in scripts/build_timeline.py if needed.