TikTok / Douyin Short Video Generator
You (the agent) do the creative work — script, scene breakdown, search
keywords, style choices, and quality review. The scripts in scripts/ do the
mechanical work — TTS with word timestamps, asset search & download,
caption rendering, ffmpeg composition, loudness normalization.
Everything runs with core ffmpeg only (no libass/drawtext needed) plus a
Python venv. Works with zero API keys; PEXELS_API_KEY / PIXABAY_API_KEY
env vars unlock real stock video clips (better) when present.
Workflow
0. One-time setup (skip if .venv/ and assets/fonts/ exist)
bash scripts/setup.sh
1. VERIFY FACTS FIRST, then write the script + storyboard
Your internal knowledge may be outdated or wrong — never put an unverified
claim in a video. Before writing any scene text, use your web search tool to
check every concrete claim: numbers, statistics, records, prices, dates,
rankings, and every "first / biggest / only / fastest" statement — and
anything time-sensitive (news, products, versions, "今年/最新") regardless of
how confident you feel. Rules:
- A claim goes into the script only as a source states it — adjust your
wording to the source, not the other way around.
- Can't verify it? Replace it with a claim you can, or cut it. A wrong number
in a published video is worse than a weaker hook.
- Record the URLs you relied on in the storyboard's
"sources": [...] —
they are echoed in review/report.txt so the user can audit the video.
- If you genuinely have no web access, stick to long-established textbook
facts, avoid specific figures, and tell the user the video is unverified.
Read references/writing-guide.md for the script formulas (hook types,
pacing, scene structure). Then create projects/<slug>/storyboard.json:
{
"title": "深海里最诡异的5种生物",
"lang": "zh",
"aspect": "9:16",
"voice": "zh-CN-YunjianNeural",
"rate": "+10%",
"caption_style": "karaoke",
"bgm": {"mood": "mystery", "gain_db": -16},
"hook": {"text": "深海禁区", "seconds": 2.3},
"scenes": [
{
"text": "你知道吗?在阳光永远照不到的深海,藏着比科幻电影更诡异的生物。",
"keywords": ["deep sea NOAA ocean exploration", "submarine dark ocean"],
"providers": ["openverse", "wikimedia"],
"badge": null,
"effect": "kb_in"
},
{
"text": "第一名,鮟鱇鱼……",
"keywords": ["humpback anglerfish", "anglerfish museum"],
"badge": "第1名"
}
]
}
Field reference:
| field |
values |
notes |
lang |
zh | en |
sets caption grouping + default voice |
aspect |
9:16 (default) | 16:9 | 1:1 |
|
voice |
any edge-tts voice |
zh: zh-CN-YunjianNeural(磁性男声) zh-CN-XiaoxiaoNeural(女声) zh-CN-YunxiNeural(阳光男声); en: en-US-ChristopherNeural en-US-AriaNeural en-US-GuyNeural |
rate |
e.g. +10% |
营销号 pacing: zh +8%+15%, en +5%+10% |
caption_style |
karaoke | pop | none |
karaoke = word-by-word highlight (recommended) |
bgm |
{"mood": ...} | {"query": "style words"} | {"file": "path.mp3"} | {"mood":"none"} |
Default to a mood: the curated table (upbeat funny inspiring chill tech mystery epic sad horror) is the exact MacLeod staples countless 营销号 channels run on — proven, safe, varied per project. Reach for query only when no mood fits the content's style (or the user names a genre): it searches real CC beat music (ccMixter style tags + Openverse/Jamendo; ND/NC excluded) and every candidate is screened by the beat tracker — no confident 75–175 BPM groove, no pick. Style words that map to tags: trap/phonk, hip hop, lofi/chill, edm/dance, epic/cinematic, quirky. You may set both — query is tried first, mood is the fallback. file = the user's own track |
bgm.vibe |
"spedup" | "slowed" | unset |
抖音 signature sound: spedup ≈ 1.25× with raised pitch (卡点/盘点视频的主流处理), slowed = slowed + reverb (情感/氛围向). Applied to any bgm source incl. file; beat grid is re-analyzed after processing so 卡点 stays accurate |
beat_sync |
true (default) | false |
the BGM is beat-analyzed and every scene/shot cut snaps onto the beat grid (卡点) when the grid is confident; VO is never truncated, only the breathing pads flex |
hook |
{"text","seconds"} |
big top title card shown at the start, ≤ 8 chars/words |
sticky_title |
{"text": ...} |
optional persistent topic bar at the top; off by default — only add if the user asks for one |
sfx |
true (default) | false |
whoosh sound on scene transitions |
scene keywords |
list of English, concrete-noun queries |
each entry = one shot (visual); the video cuts to a new visual every ~3s, so give 2–3 queries for scenes longer than ~4s (extra shots are auto-added cycling your queries if you give fewer) |
scene badge |
e.g. "第1名" / "TOP 1" |
big stamped label shown at the scene start — use for listicles |
scene providers |
list |
keyless images: openverse wikimedia nasa; keyless real video: wikimedia_video (Commons clips, server transcodes) nasa_video (PD space/science) archive_video (Prelinger public-domain historical film); with keys: pexels_video pexels_photo pixabay_video |
scene effect |
auto kb_in kb_out pan_left pan_right static |
first shot's Ken Burns motion; later shots auto-cycle |
scene emphasis |
list of substrings |
permanently highlighted words (pop style) |
scene media |
file path |
bypass search, use your own file (e.g. one you downloaded yourself) |
Rules of thumb (publish bar): total 45–75s, 6–9 scenes, one idea per scene,
2–3 shots per scene. Scene text 20–45 Chinese chars / 14–30 English words.
Anything under ~40s or with single-shot scenes throughout will feel thin — don't
ship it.
2. Run the pipeline
.venv/bin/python scripts/pipeline.py projects/<slug>
Or stage by stage: tts.py → assets.py → bgm.py → compose.py → check.py.
3. REVIEW — this is what makes the difference (mandatory)
Free-image search is imperfect. Always view these two files with your image
tool and judge them like a human editor:
projects/<slug>/media/assets_sheet.jpg — after the assets stage.
Shots are labeled 01a 01b 02a …. For every shot that does not clearly
match the narration, refetch it:
.venv/bin/python scripts/assets.py projects/<slug> --scene 3 --shot 2 --keywords "better english nouns"
(Refetching automatically blacklists the rejected asset. Repeat until all
scenes match. You may also download an image yourself with your own tools
and point scene media at it.)
The pipeline also text-audits every shot's source title against its query
and prints [!] OFF-TOPIC flags. If you visually confirmed a flagged
shot is actually right (e.g. an untitled but perfect photo), set
"approved": true on that shot in media/manifest.json to clear it —
your eyes outrank the text audit. Never blind-approve.
projects/<slug>/review/contact_sheet.jpg — after compose+check.
Verify: captions readable & synced, no wrong/ugly frames, hook visible.
Fix issues (edit storyboard → rerun compose, it's fast) until it looks
like a video a 营销号 editor would actually publish.
review/report.txt contains duration/loudness checks and a ready-to-paste
attribution block (CC-BY sources + music credit) — always give it to the
user together with final.mp4.
Asset search tips (biggest quality lever)
- Keywords must be English, 1–4 words, concrete visible nouns
("humpback anglerfish", "scuba diver silhouette"), never abstract concepts
("mystery", "success").
wikimedia is best for animals/science/history/places; openverse
(Flickr etc.) for lifestyle/scenery/mood; nasa for space; keys unlock
pexels_video/pixabay_video real footage — prefer those when available.
- Add a context word to disambiguate: "NOAA", "museum", "aquarium", "macro".
- If a scene keeps failing, change the visual concept, not just the words
(e.g. for "5% explored" show a diver silhouette, not "statistics").
wikimedia_video searches support negative terms — Commons weather/space
queries drown in satellite loops, so write e.g. "lightning storm -CIRA -satellite -JPSS". Non-English words unlock extra footage ("Blitz
Gewitter", "tormenta rayos"). Prefer named subjects ("Fagradalsfjall lava
drone") over generic nouns, and always eyeball video thumbs in the sheet:
watch out for burned-in captions, watermarks, tutorials and CGI promos.
Troubleshooting
edge-tts network errors: it retries 4×; rerun tts.py if it still fails.
- BGM download fails: rerun
bgm.py (retries + cache), or --mood none,
or drop an mp3 into assets/bgm/ and set bgm.file.
- BGM style doesn't fit: refine the query (
bgm.py <dir> --query "..." then
recompose with --skip-tts --skip-assets). For a 真正的当下热门 sound,
deliver with mood: none (or low gain) and tell the user to add the
trending track from TikTok/抖音's in-app licensed music library at upload
time — that is both legal and algorithm-favored. Cut timing will still be
clean because scene pacing is unchanged.
check.py prints a "cuts land on the beat" line — if beat sync was skipped
(low grid confidence, e.g. beatless ambient music), pick a steadier track
(--query "trap beat" etc.) and recompose.
- A provider erroring/empty is fine — others cover it; check per-scene logs.
- Recompose after any storyboard edit is cheap (
--skip-tts --skip-assets
via pipeline.py, or run compose.py directly). Changing scene text
requires rerunning tts.py.
- Emoji in captions render on macOS (Apple Color Emoji); on Linux they are
dropped unless NotoColorEmoji is installed. Never rely on emoji for meaning.
Deliver
Give the user: final.mp4, the attribution block from review/report.txt,
and (if asked) review/cover.jpg as the cover image.
1---2name: tiktok-video3description: Generate complete TikTok / Douyin(抖音) / YouTube Shorts / Reels short videos from a one-line brief, end to end: script writing, free stock footage & image download, TTS voiceover (Chinese & English), word-level karaoke captions, background music, and ffmpeg composition into a finished 1080x1920 MP4. Use when the user asks to create, make, or generate a short video (短视频/抖音视频/TikTok video) on any topic.4---56# TikTok / Douyin Short Video Generator78You (the agent) do the **creative** work — script, scene breakdown, search9keywords, style choices, and quality review. The scripts in `scripts/` do the10**mechanical** work — TTS with word timestamps, asset search & download,11caption rendering, ffmpeg composition, loudness normalization.1213Everything runs with core ffmpeg only (no libass/drawtext needed) plus a14Python venv. Works with zero API keys; `PEXELS_API_KEY` / `PIXABAY_API_KEY`15env vars unlock real stock video clips (better) when present.1617## Workflow1819### 0. One-time setup (skip if `.venv/` and `assets/fonts/` exist)2021```bash22bash scripts/setup.sh23```2425### 1. VERIFY FACTS FIRST, then write the script + storyboard2627**Your internal knowledge may be outdated or wrong — never put an unverified28claim in a video.** Before writing any scene text, use your web search tool to29check every concrete claim: numbers, statistics, records, prices, dates,30rankings, and every "first / biggest / only / fastest" statement — and31anything time-sensitive (news, products, versions, "今年/最新") regardless of32how confident you feel. Rules:3334- A claim goes into the script **only as a source states it** — adjust your35 wording to the source, not the other way around.36- Can't verify it? Replace it with a claim you can, or cut it. A wrong number37 in a published video is worse than a weaker hook.38- Record the URLs you relied on in the storyboard's `"sources": [...]` —39 they are echoed in `review/report.txt` so the user can audit the video.40- If you genuinely have no web access, stick to long-established textbook41 facts, avoid specific figures, and tell the user the video is unverified.4243Read `references/writing-guide.md` for the script formulas (hook types,44pacing, scene structure). Then create `projects/<slug>/storyboard.json`:4546```json47{48 "title": "深海里最诡异的5种生物",49 "lang": "zh",50 "aspect": "9:16",51 "voice": "zh-CN-YunjianNeural",52 "rate": "+10%",53 "caption_style": "karaoke",54 "bgm": {"mood": "mystery", "gain_db": -16},55 "hook": {"text": "深海禁区", "seconds": 2.3},56 "scenes": [57 {58 "text": "你知道吗?在阳光永远照不到的深海,藏着比科幻电影更诡异的生物。",59 "keywords": ["deep sea NOAA ocean exploration", "submarine dark ocean"],60 "providers": ["openverse", "wikimedia"],61 "badge": null,62 "effect": "kb_in"63 },64 {65 "text": "第一名,鮟鱇鱼……",66 "keywords": ["humpback anglerfish", "anglerfish museum"],67 "badge": "第1名"68 }69 ]70}71```7273Field reference:7475| field | values | notes |76|---|---|---|77| `lang` | `zh` \| `en` | sets caption grouping + default voice |78| `aspect` | `9:16` (default) \| `16:9` \| `1:1` | |79| `voice` | any edge-tts voice | zh: `zh-CN-YunjianNeural`(磁性男声) `zh-CN-XiaoxiaoNeural`(女声) `zh-CN-YunxiNeural`(阳光男声); en: `en-US-ChristopherNeural` `en-US-AriaNeural` `en-US-GuyNeural` |80| `rate` | e.g. `+10%` | 营销号 pacing: zh `+8%`~`+15%`, en `+5%`~`+10%` |81| `caption_style` | `karaoke` \| `pop` \| `none` | karaoke = word-by-word highlight (recommended) |82| `bgm` | `{"mood": ...}` \| `{"query": "style words"}` \| `{"file": "path.mp3"}` \| `{"mood":"none"}` | **Default to a `mood`**: the curated table (upbeat funny inspiring chill tech mystery epic sad horror) is the exact MacLeod staples countless 营销号 channels run on — proven, safe, varied per project. **Reach for `query` only when no mood fits the content's style** (or the user names a genre): it searches real CC beat music (ccMixter style tags + Openverse/Jamendo; ND/NC excluded) and every candidate is screened by the beat tracker — no confident 75–175 BPM groove, no pick. Style words that map to tags: trap/phonk, hip hop, lofi/chill, edm/dance, epic/cinematic, quirky. You may set both — query is tried first, mood is the fallback. `file` = the user's own track |83| `bgm.vibe` | `"spedup"` \| `"slowed"` \| unset | 抖音 signature sound: `spedup` ≈ 1.25× with raised pitch (卡点/盘点视频的主流处理), `slowed` = slowed + reverb (情感/氛围向). Applied to any bgm source incl. `file`; beat grid is re-analyzed after processing so 卡点 stays accurate |84| `beat_sync` | `true` (default) \| `false` | the BGM is beat-analyzed and every scene/shot cut snaps onto the beat grid (卡点) when the grid is confident; VO is never truncated, only the breathing pads flex |85| `hook` | `{"text","seconds"}` | big top title card shown at the start, ≤ 8 chars/words |86| `sticky_title` | `{"text": ...}` | optional persistent topic bar at the top; off by default — only add if the user asks for one |87| `sfx` | `true` (default) \| `false` | whoosh sound on scene transitions |88| scene `keywords` | list of **English, concrete-noun** queries | **each entry = one shot (visual)**; the video cuts to a new visual every ~3s, so give 2–3 queries for scenes longer than ~4s (extra shots are auto-added cycling your queries if you give fewer) |89| scene `badge` | e.g. `"第1名"` / `"TOP 1"` | big stamped label shown at the scene start — use for listicles |90| scene `providers` | list | keyless images: `openverse` `wikimedia` `nasa`; **keyless real video**: `wikimedia_video` (Commons clips, server transcodes) `nasa_video` (PD space/science) `archive_video` (Prelinger public-domain historical film); with keys: `pexels_video` `pexels_photo` `pixabay_video` |91| scene `effect` | `auto` `kb_in` `kb_out` `pan_left` `pan_right` `static` | first shot's Ken Burns motion; later shots auto-cycle |92| scene `emphasis` | list of substrings | permanently highlighted words (pop style) |93| scene `media` | file path | bypass search, use your own file (e.g. one you downloaded yourself) |9495**Rules of thumb (publish bar): total 45–75s, 6–9 scenes, one idea per scene,962–3 shots per scene.** Scene text 20–45 Chinese chars / 14–30 English words.97Anything under ~40s or with single-shot scenes throughout will feel thin — don't98ship it.99100### 2. Run the pipeline101102```bash103.venv/bin/python scripts/pipeline.py projects/<slug>104```105106Or stage by stage: `tts.py` → `assets.py` → `bgm.py` → `compose.py` → `check.py`.107108### 3. REVIEW — this is what makes the difference (mandatory)109110Free-image search is imperfect. **Always view these two files with your image111tool and judge them like a human editor:**1121131. `projects/<slug>/media/assets_sheet.jpg` — after the assets stage.114 Shots are labeled `01a 01b 02a …`. For every shot that does not clearly115 match the narration, refetch it:116117 ```bash118 .venv/bin/python scripts/assets.py projects/<slug> --scene 3 --shot 2 --keywords "better english nouns"119 ```120121 (Refetching automatically blacklists the rejected asset. Repeat until all122 scenes match. You may also download an image yourself with your own tools123 and point scene `media` at it.)124125 The pipeline also text-audits every shot's source title against its query126 and prints `[!] OFF-TOPIC` flags. If you **visually confirmed** a flagged127 shot is actually right (e.g. an untitled but perfect photo), set128 `"approved": true` on that shot in `media/manifest.json` to clear it —129 your eyes outrank the text audit. Never blind-approve.1301312. `projects/<slug>/review/contact_sheet.jpg` — after compose+check.132 Verify: captions readable & synced, no wrong/ugly frames, hook visible.133 Fix issues (edit storyboard → rerun compose, it's fast) until it looks134 like a video a 营销号 editor would actually publish.135136`review/report.txt` contains duration/loudness checks and a ready-to-paste137**attribution block** (CC-BY sources + music credit) — always give it to the138user together with `final.mp4`.139140### Asset search tips (biggest quality lever)141142- Keywords must be **English**, 1–4 words, **concrete visible nouns**143 ("humpback anglerfish", "scuba diver silhouette"), never abstract concepts144 ("mystery", "success").145- `wikimedia` is best for animals/science/history/places; `openverse`146 (Flickr etc.) for lifestyle/scenery/mood; `nasa` for space; keys unlock147 `pexels_video`/`pixabay_video` real footage — prefer those when available.148- Add a context word to disambiguate: "NOAA", "museum", "aquarium", "macro".149- If a scene keeps failing, change the visual concept, not just the words150 (e.g. for "5% explored" show a diver silhouette, not "statistics").151- `wikimedia_video` searches support negative terms — Commons weather/space152 queries drown in satellite loops, so write e.g. `"lightning storm -CIRA153 -satellite -JPSS"`. Non-English words unlock extra footage ("Blitz154 Gewitter", "tormenta rayos"). Prefer named subjects ("Fagradalsfjall lava155 drone") over generic nouns, and always eyeball video thumbs in the sheet:156 watch out for burned-in captions, watermarks, tutorials and CGI promos.157158### Troubleshooting159160- `edge-tts` network errors: it retries 4×; rerun `tts.py` if it still fails.161- BGM download fails: rerun `bgm.py` (retries + cache), or `--mood none`,162 or drop an mp3 into `assets/bgm/` and set `bgm.file`.163- BGM style doesn't fit: refine the query (`bgm.py <dir> --query "..."` then164 recompose with `--skip-tts --skip-assets`). For a *真正的当下热门* sound,165 deliver with `mood: none` (or low gain) and tell the user to add the166 trending track from TikTok/抖音's in-app licensed music library at upload167 time — that is both legal and algorithm-favored. Cut timing will still be168 clean because scene pacing is unchanged.169- `check.py` prints a "cuts land on the beat" line — if beat sync was skipped170 (low grid confidence, e.g. beatless ambient music), pick a steadier track171 (`--query "trap beat"` etc.) and recompose.172- A provider erroring/empty is fine — others cover it; check per-scene logs.173- Recompose after any storyboard edit is cheap (`--skip-tts --skip-assets`174 via pipeline.py, or run compose.py directly). Changing scene *text*175 requires rerunning `tts.py`.176- Emoji in captions render on macOS (Apple Color Emoji); on Linux they are177 dropped unless NotoColorEmoji is installed. Never rely on emoji for meaning.178179### Deliver180181Give the user: `final.mp4`, the attribution block from `review/report.txt`,182and (if asked) `review/cover.jpg` as the cover image.