
Video Podcast Maker
- 102 installs
- 1.5k repo stars
- Updated August 1, 2026
- agents365-ai/video-podcast-maker
This is a copy of video-podcast-maker by agents365-ai - installs and ranking accrue to the original listing.
video-podcast-maker is a Claude Code skill that turns a topic into a narrated 4K explainer video via a research to script to TTS to Remotion to MP4 pipeline.
About
video-podcast-maker is a Claude Code skill that turns a topic into a narrated explainer or knowledge-summary video through a research, script, TTS, Remotion, and FFmpeg pipeline, outputting 4K MP4 with background music. A developer uses it to produce topic-driven videos for Bilibili, YouTube, Xiaohongshu, or Douyin. It depends on the remotion-best-practices skill and supports several TTS backends.
- Automated topic-to-4K-video pipeline: research to script to TTS to Remotion to MP4 + BGM
- 15-step workflow producing narrated explainer videos for Bilibili, YouTube, Douyin, and more
- Multiple TTS backends (Edge free default, Azure, Doubao, ElevenLabs, OpenAI, Google)
Video Podcast Maker by the numbers
- 102 all-time installs (skills.sh)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
video-podcast-maker capabilities & compatibility
Free with the default Edge TTS backend; paid backends (Azure, ElevenLabs, OpenAI) need their own API keys.
- Capabilities
- video generation · text to speech · explainer video · thumbnail generation
- Use cases
- video generation · transcription
- Platforms
- macOS · Linux
- Pricing
- Freemium
What video-podcast-maker says it does
Produces 4K video via research → script → TTS → Remotion → MP4 + BGM.
Automated pipeline for **4K Bilibili horizontal knowledge videos** from a topic. Coding agent + TTS backend + Remotion + FFmpeg.
npx skills add https://github.com/agents365-ai/video-podcast-maker --skill video-podcast-makerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 102 |
|---|---|
| repo stars | ★ 1.5k |
| Last updated | August 1, 2026 |
| Repository | agents365-ai/video-podcast-maker ↗ |
What it does
Turn a topic into a narrated 4K explainer video via a research, script, TTS, Remotion, and FFmpeg pipeline.
Who is it for?
Automated topic-driven narrated explainer, podcast, or knowledge-summary videos for platforms like Bilibili and YouTube.
Skip if: Generic video editing, trimming, format conversion, color grading, or non-narrative video tasks.
When should I use this skill?
The user gives a topic and wants an automated narrated explainer, podcast, or knowledge-summary video.
What you get
A 4K MP4 explainer video with narration, BGM, thumbnails, and publish metadata.
- A 4K MP4 explainer video with BGM
- Thumbnails (16:9 and 4:3)
- publish_info.md metadata
By the numbers
- 15-step workflow
- 7 TTS backends
- 4K video output
Files
REQUIRED: Load Remotion Best Practices First
>
This skill depends on remotion-best-practices. You MUST invoke it before proceeding:```
Invoke the skill/tool named: remotion-best-practices
```
Video Podcast Maker
Automated pipeline for 4K Bilibili horizontal knowledge videos from a topic. Coding agent + TTS backend + Remotion + FFmpeg.
Contents
- Bootstrap — update check + prerequisites (run before Step 1)
- Execution Modes — Auto vs Interactive, default decisions
- Workflow — the 15 steps + phase-file pointers + mandatory stops
- Hard Rules — non-negotiable production constraints + output specs
- Per-Video Layout — directory structure,
--public-dir, naming - Additional Resources — when to load each
references/file - User Preferences
- Troubleshooting
---
Bootstrap
Resolve SKILL_DIR to the directory containing this SKILL.md. If your agent exposes a built-in skill directory variable (e.g. ${CLAUDE_SKILL_DIR}), map it to SKILL_DIR.
SKILL_DIR="${SKILL_DIR:-${CLAUDE_SKILL_DIR}}"
# 1. Update check (notify-only, throttled to 24h)
"${SKILL_DIR}/scripts/check_update.sh"
# 2. Prerequisites (CLIs + backend env vars)
python3 "${SKILL_DIR}/scripts/check_prereqs.py"`check_update.sh` output:
UPDATE_AVAILABLE vX.Y.Z -> vA.B.C— tell the user the version delta and ask before runninggit -C "${SKILL_DIR}" pull --ff-only. Notify-only by design — never pull without consent (the skill directory belongs to the user).UP_TO_DATE/SKIPPED_RECENT_CHECK/MANUAL_INSTALL— continue silently.
Prereqs failures — see README.md for setup. The check is backend-aware (resolves TTS_BACKEND env → user_prefs.json global.tts.backend → edge default), so only env vars required by the active backend are validated.
Design Learning shortcut: If the user provides a reference video/image or asks to save/list/delete style profiles, see references/design-learning.md instead of running the workflow below.
---
Execution Modes
Detect at workflow start:
- "Make a video about..." / no special instructions → Auto Mode (default)
- "I want to control each step" / "interactive" → Interactive Mode
Auto Mode defaults
Full pipeline with sensible defaults. Mandatory stop at Step 9 (Studio review); Step 10 (4K render) only fires when the user says "render 4K" / "render final".
| Step | Decision | Auto Default |
|---|---|---|
| 3 | Title position | top-center |
| 5 | Media assets | Skip (text-only animations) |
| 7 | Thumbnail method | Remotion-generated (16:9 + 4:3) |
| 9 | Outro animation | Pre-made MP4 (white/black by theme) |
| 12 | Subtitle method | Remotion-native (skip legacy FFmpeg burn) |
| 14 | Cleanup | Auto-clean temp files |
Override any default in the initial request:
- "make a video about AI, burn subtitles" → auto + subtitles on
- "use dark theme, AI thumbnails" → auto + dark + imagen
- "need screenshots" → auto + media collection enabled
Interactive Mode
Prompts at each decision point.
---
Workflow
At Step 1 start, create one task per step in your agent's tracker (Claude Code TaskCreate / Codex todo list / equivalent). Mark in_progress on start, completed on finish. Files in videos/{name}/ are the durable record — if interrupted, inspect the directory to determine where to resume.
| # | Step | Output | Phase file |
|---|---|---|---|
| 1 | Define topic direction | topic_definition.md | workflow-script.md |
| 2 | Research topic | topic_research.md | workflow-script.md |
| 3 | Design 5-7 sections | (in-memory) | workflow-script.md |
| 4 | Write narration script | podcast.txt | workflow-script.md |
| 4.5 | Pronunciation pre-flight (zh-CN) | phonemes.json | workflow-script.md |
| 5 | Collect media (Auto: skip) | media_manifest.json | workflow-production.md |
| 6 | Generate publish info (Part 1) | publish_info.md | workflow-production.md |
| 7 | Generate thumbnails (16:9 + 4:3) | thumbnail_*.png | workflow-production.md |
| 8 | Generate TTS audio | podcast_audio.wav, timing.json | workflow-production.md |
| 9 | Remotion composition + Studio preview | — | workflow-production.md |
| 10 | Render 4K video (only on user request) | output.mp4 | workflow-production.md |
| 11 | Mix background music | video_with_bgm.mp4 | workflow-production.md |
| 12 | Finalize (optional legacy subtitle burn) | final_video.mp4 | workflow-publish.md |
| 13 | Complete publish info (Part 2) | chapter timestamps | workflow-publish.md |
| 14 | Verify output (scripts/verify_output.py) | — | workflow-publish.md |
| 15 | Generate vertical shorts (optional) | shorts/ | workflow-publish.md |
Mandatory stops (bold rows above):
- Step 9 — Studio review. MUST launch
npx remotion studioand wait for user feedback before rendering. NEVER render 4K until the user explicitly confirms ("render 4K" / "render final"). - Step 14 — `verify_output.py`. MUST pass before declaring the video done. Exit 0 = green; exit 2 = warnings still publishable. Auto-fixes common omissions (creates
final_video.mp4if missing). For machine-readable output add--format json(auto when piped).
Pre-render audit (recommended) — before Step 9:
python3 ${SKILL_DIR}/scripts/audit_beat_sync.py <Video.tsx> <timing.json>Flags beats that drift > 1.5s from narration. Especially important for kinetic-typography videos.
Validation Checkpoints
| After Step | Check |
|---|---|
| 8 (TTS) | podcast_audio.wav plays · timing.json covers all sections · SRT is UTF-8 |
| 10 (Render) | output.mp4 is 3840×2160 · audio-video sync · no black frames |
| 14 (Verify) | verify_output.py exits 0 (or 2 with reviewed warnings) |
---
Hard Rules
| Rule | Requirement |
|---|---|
| Single Project | All videos under videos/{name}/ in user's Remotion project. NEVER create a new project per video. |
| 4K Output | 3840×2160 (or 2160×3840 vertical), use scale(2) wrapper over 1920×1080 design space |
| Audio Sync | All animations driven by timing.json timestamps |
| Thumbnail | MUST generate both 16:9 (1920×1080) AND 4:3 (1200×900) — see design-guide.md |
| Studio Before Render | MUST launch remotion studio for review. NEVER render 4K until user explicitly confirms. |
| `--public-dir` | Every Remotion command uses --public-dir videos/{name}/ |
Visual minimums (text sizes, content width, safe zones, animation safety) live in references/design-guide.md. MUST load before Step 9.
Output Specs
| Parameter | Horizontal (16:9) | Vertical (9:16) |
|---|---|---|
| Resolution | 3840×2160 (4K) | 2160×3840 (4K) |
| Frame rate | 30 fps | 30 fps |
| Encoding | H.264, 16Mbps | H.264, 16Mbps |
| Audio | AAC, 192kbps | AAC, 192kbps |
| Duration | 1-15 min | 60-90s (highlight) |
---
Per-Video Layout
project-root/ # Remotion project root
├── src/remotion/ # Remotion source (Root.tsx, compositions, index.ts)
├── videos/{video-name}/ # Per-video assets (the agent's working dir)
│ ├── topic_definition.md # Step 1
│ ├── topic_research.md # Step 2
│ ├── podcast.txt # Step 4: narration script
│ ├── phonemes.json # Step 4.5: zh-CN pronunciation overrides
│ ├── podcast_audio.wav # Step 8: TTS audio
│ ├── podcast_audio.srt # Step 8: subtitles
│ ├── timing.json # Step 8: timeline (drives animations)
│ ├── thumbnail_*.png # Step 7
│ ├── output.mp4 # Step 10: 4K render (no BGM)
│ ├── video_with_bgm.mp4 # Step 11
│ ├── final_video.mp4 # Step 12: final output
│ └── bgm.mp3 # Background music
└── remotion.config.ts--public-dir per video
Remotion commands MUST use --public-dir videos/{name}/ — each video's assets stay in its own directory, no copy to public/. Enables parallel renders.
npx remotion studio src/remotion/index.ts --public-dir videos/{name}/
npx remotion render src/remotion/index.ts CompositionId videos/{name}/output.mp4 --public-dir videos/{name}/ --video-bitrate 16M
npx remotion still src/remotion/index.ts Thumbnail16x9 videos/{name}/thumbnail.png --public-dir videos/{name}/Naming
- Video name `{video-name}`: lowercase English, hyphen-separated (e.g.
reference-manager-comparison) - Section name `{section}`: lowercase English, underscore-separated, matches
[SECTION:xxx] - Thumbnail naming (16:9 AND 4:3 both required):
| Type | 16:9 | 4:3 |
|---|---|---|
| Remotion | thumbnail_remotion_16x9.png | thumbnail_remotion_4x3.png |
| AI | thumbnail_ai_16x9.png | thumbnail_ai_4x3.png |
---
Additional Resources
Load on demand — do NOT load all at once:
| File | Load when |
|---|---|
| references/workflow-script.md | Steps 1-4 (topic → script) |
| references/workflow-production.md | Steps 5-11 (media → TTS → Remotion → render → BGM) |
| references/workflow-publish.md | Steps 12-15 (subtitles, publish, cleanup, shorts) |
| references/design-guide.md | MUST load before Step 9 — visual minimums, typography, animation safety |
| references/design-learning.md | User provides a reference video/image, or manages style profiles |
| references/azure-tts-pitfalls.md | Choosing Azure voice/style, debugging hoarse/glitchy audio |
| references/troubleshooting.md | On error, or user asks about preferences/BGM |
| templates/presets/kinetic-typography/ | Bold type-driven preset (opinion / argument / declaration videos) |
| examples/ | Reference for composition structure and timing.json format |
Script suite dispatcher
All scripts under ${SKILL_DIR}/scripts/ are reachable through one hierarchical entry point:
python3 ${SKILL_DIR}/scripts/cli.py --help # list resources
python3 ${SKILL_DIR}/scripts/cli.py <resource> --help # list actions
python3 ${SKILL_DIR}/scripts/cli.py <resource> <action> --help # forwards to underlying script
python3 ${SKILL_DIR}/scripts/cli.py schema [<method>] # JSON parameter schemaRoutes: tts run|validate, verify, audit beats, shorts gen, design list|show|delete|add, prereqs, prefs get|migrate|backend|bgm-path, schema [<method>]. Direct script invocation (python3 scripts/<name>.py ...) keeps working — the dispatcher is additive.
---
User Preferences
Skill auto-learns and applies preferences. Full commands and learning details: references/troubleshooting.md.
- Storage:
user_prefs.json(auto-created fromuser_prefs.template.json, schema inprefs_schema.json). - Priority:
Root.tsx defaults < global < topic_patterns[type] < current instructions. - User commands: "show preferences" · "reset preferences" · "save as X default".
---
Troubleshooting
See references/troubleshooting.md on errors, BGM options, preference learning, design-learning issues.
# TTS Configuration
TTS_BACKEND=edge # Options: edge, azure, doubao, cosyvoice, elevenlabs, openai, google
TTS_RATE="+5%"
# Azure Speech (if using azure backend)
# AZURE_SPEECH_KEY=your-key-here
# AZURE_SPEECH_REGION=eastasia
# Volcengine Doubao (if using doubao backend)
# VOLCENGINE_APPID=your-appid
# VOLCENGINE_ACCESS_TOKEN=your-token
# Alibaba CosyVoice (if using cosyvoice backend)
# DASHSCOPE_API_KEY=your-key
# ElevenLabs (if using elevenlabs backend)
# ELEVENLABS_API_KEY=your-key
# OpenAI (if using openai backend)
# OPENAI_API_KEY=your-key
# Google Cloud TTS (if using google backend)
# GOOGLE_TTS_API_KEY=your-key
# Optional: AI thumbnails
# GEMINI_API_KEY=your-key
# DASHSCOPE_API_KEY=your-key (shared with CosyVoice)
3.10
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#6366F1', 'primaryTextColor': '#fff', 'primaryBorderColor': '#4F46E5', 'lineColor': '#94A3B8', 'secondaryColor': '#F1F5F9', 'tertiaryColor': '#fff', 'fontSize': '14px', 'fontFamily': 'system-ui, sans-serif'}}}%%
flowchart LR
subgraph PLAN["📋 Planning"]
direction TB
A[/"🎯 Topic"/] --> B["💡 Brainstorm"]
B --> C["🔍 Research"]
C --> D["📐 Sections"]
end
subgraph CONTENT["✍️ Content"]
direction TB
E["📝 Script"] --> F["🏷️ Publish Info"]
F --> G["🖼️ Thumbnail"]
end
subgraph PROD["🎬 Production"]
direction TB
H["🎙️ TTS Audio"] --> I["⚛️ Remotion"]
I --> J["👁️ Preview"]
J --> K["🎞️ Render 4K"]
end
subgraph POST["🎛️ Post"]
direction TB
L["🎵 Mix BGM"] --> M["📜 Subtitles"]
end
subgraph OUT["📦 Output"]
direction TB
N["✅ Verify"] --> O["🧹 Cleanup"]
O --> P[/"🎉 MP4"/]
end
PLAN --> CONTENT --> PROD --> POST --> OUT
style A fill:#10B981,stroke:#059669,color:#fff
style P fill:#10B981,stroke:#059669,color:#fff
style J fill:#F59E0B,stroke:#D97706,color:#fff
style PLAN fill:#EEF2FF,stroke:#6366F1,color:#3730A3
style CONTENT fill:#FEF3C7,stroke:#F59E0B,color:#92400E
style PROD fill:#DBEAFE,stroke:#3B82F6,color:#1E40AF
style POST fill:#FCE7F3,stroke:#EC4899,color:#9D174D
style OUT fill:#D1FAE5,stroke:#10B981,color:#065F46
{
"name": "video-podcast-maker",
"version": "2.3.0",
"description": "Automated pipeline to create professional video podcasts from a topic",
"engines": {
"node": ">=18"
},
"scripts": {
"studio": "remotion studio src/remotion/index.ts",
"render": "remotion render src/remotion/index.ts",
"build": "remotion bundle src/remotion/index.ts"
},
"dependencies": {
"@remotion/cli": "^4.0.438",
"@remotion/lottie": "^4.0.438",
"@remotion/media-utils": "^4.0.438",
"@remotion/paths": "^4.0.438",
"@remotion/player": "^4.0.438",
"@remotion/shapes": "^4.0.438",
"@remotion/transitions": "^4.0.438",
"lottie-web": "^5.13.0",
"lucide-react": "^0.577.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"remotion": "^4.0.438",
"zod": "^3.22.0"
},
"devDependencies": {
"@types/react": "^18.2.0",
"typescript": "^5.3.0"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
{
"_comment": "Template for global phoneme dictionary. Copied to phonemes.json on first run. New entries added here are merged into existing phonemes.json without overwriting user customizations. Use whole-word keys (not single chars). Pinyin uses tone marks; the loader converts to SAPI for Azure TTS. Other backends (Edge/Doubao/CosyVoice) ignore phonemes — see references/troubleshooting.md.",
"_grouping": "Entries are grouped by polyphone character for human readability. The runtime sorts by length descending and matches longest-first.",
"_行_háng": "row/line/profession",
"一行命令": "yì háng mìng lìng",
"一行代码": "yì háng dài mǎ",
"命令行": "mìng lìng háng",
"代码行": "dài mǎ háng",
"一行": "yì háng",
"单行": "dān háng",
"多行": "duō háng",
"几行": "jǐ háng",
"行数": "háng shù",
"行业": "háng yè",
"行情": "háng qíng",
"行家": "háng jiā",
"银行": "yín háng",
"外行": "wài háng",
"内行": "nèi háng",
"_行_xíng": "walk/execute/proceed",
"执行器": "zhí xíng qì",
"执行": "zhí xíng",
"运行": "yùn xíng",
"并行": "bìng xíng",
"可行": "kě xíng",
"行为": "xíng wéi",
"行动": "xíng dòng",
"行走": "xíng zǒu",
"行驶": "xíng shǐ",
"进行": "jìn xíng",
"推行": "tuī xíng",
"实行": "shí xíng",
"施行": "shī xíng",
"履行": "lǚ xíng",
"流行": "liú xíng",
"旅行": "lǚ xíng",
"出行": "chū xíng",
"自行": "zì xíng",
"_重_chóng": "again/repeat",
"重做": "chóng zuò",
"重新": "chóng xīn",
"重复": "chóng fù",
"重试": "chóng shì",
"重置": "chóng zhì",
"重启": "chóng qǐ",
"重写": "chóng xiě",
"重建": "chóng jiàn",
"重装": "chóng zhuāng",
"重申": "chóng shēn",
"重塑": "chóng sù",
"重温": "chóng wēn",
"重逢": "chóng féng",
"_重_zhòng": "heavy/important",
"重要": "zhòng yào",
"重点": "zhòng diǎn",
"重视": "zhòng shì",
"重大": "zhòng dà",
"重量": "zhòng liàng",
"严重": "yán zhòng",
"注重": "zhù zhòng",
"着重": "zhuó zhòng",
"慎重": "shèn zhòng",
"尊重": "zūn zhòng",
"沉重": "chén zhòng",
"比重": "bǐ zhòng",
"_长_cháng": "long/length",
"长度": "cháng dù",
"长期": "cháng qī",
"长篇": "cháng piān",
"长远": "cháng yuǎn",
"长寿": "cháng shòu",
"擅长": "shàn cháng",
"特长": "tè cháng",
"_长_zhǎng": "grow/elder/chief",
"增长": "zēng zhǎng",
"成长": "chéng zhǎng",
"生长": "shēng zhǎng",
"长大": "zhǎng dà",
"校长": "xiào zhǎng",
"部长": "bù zhǎng",
"院长": "yuàn zhǎng",
"队长": "duì zhǎng",
"班长": "bān zhǎng",
"会长": "huì zhǎng",
"家长": "jiā zhǎng",
"_为_wéi_vs_wèi": "do/become (wéi) vs because/for (wèi)",
"作为": "zuò wéi",
"认为": "rèn wéi",
"成为": "chéng wéi",
"视为": "shì wéi",
"因为": "yīn wèi",
"为了": "wèi le",
"为何": "wèi hé",
"为此": "wèi cǐ",
"_还_hái_vs_huán": "still/yet (hái) vs return (huán)",
"归还": "guī huán",
"偿还": "cháng huán",
"退还": "tuì huán",
"还款": "huán kuǎn",
"还原": "huán yuán",
"_差_chā_chà_chāi": "difference (chā) / off-by (chà) / dispatch (chāi)",
"差别": "chā bié",
"差距": "chā jù",
"差异": "chā yì",
"差错": "chā cuò",
"差不多": "chà bù duō",
"差点": "chà diǎn",
"出差": "chū chāi",
"_调_diào_vs_tiáo": "investigate/transfer (diào) vs adjust (tiáo)",
"调研": "diào yán",
"调查": "diào chá",
"调用": "diào yòng",
"调度": "diào dù",
"调动": "diào dòng",
"语调": "yǔ diào",
"声调": "shēng diào",
"调整": "tiáo zhěng",
"协调": "xié tiáo",
"调节": "tiáo jié",
"_分_fēn_vs_fèn": "split (fēn) vs portion (fèn)",
"分钟": "fēn zhōng",
"分开": "fēn kāi",
"分析": "fēn xī",
"分类": "fēn lèi",
"分别": "fēn bié",
"分享": "fēn xiǎng",
"分布": "fēn bù",
"缘分": "yuán fèn",
"部分": "bù fèn",
"充分": "chōng fèn",
"_中_zhōng_vs_zhòng": "middle (zhōng) vs hit-target (zhòng)",
"中间": "zhōng jiān",
"中央": "zhōng yāng",
"中心": "zhōng xīn",
"集中": "jí zhōng",
"中奖": "zhòng jiǎng",
"命中": "mìng zhòng",
"_处_chǔ_vs_chù": "handle (chǔ) vs place (chù)",
"处理": "chǔ lǐ",
"相处": "xiāng chǔ",
"处境": "chǔ jìng",
"到处": "dào chù",
"好处": "hǎo chù",
"用处": "yòng chù",
"长处": "cháng chù",
"短处": "duǎn chù",
"_间_jiān_vs_jiàn": "between/space (jiān) vs gap/interval (jiàn)",
"时间": "shí jiān",
"期间": "qī jiān",
"空间": "kōng jiān",
"间隔": "jiàn gé",
"间断": "jiàn duàn",
"_给_jǐ_formal": "supply (jǐ); colloquial 给我 stays gěi by default",
"给予": "jǐ yǔ",
"供给": "gōng jǐ",
"补给": "bǔ jǐ",
"_教_jiào_dominant": "education (jiào); 教书 jiāo is rare in tech contexts",
"教育": "jiào yù",
"教程": "jiào chéng",
"教师": "jiào shī",
"教练": "jiào liàn",
"教材": "jiào cái",
"教训": "jiào xùn",
"宗教": "zōng jiào",
"_模_mó_vs_mú": "form/imitate (mó) vs appearance (mú)",
"模型": "mó xíng",
"模式": "mó shì",
"规模": "guī mó",
"模块": "mó kuài",
"模拟": "mó nǐ",
"模糊": "mó hú",
"模仿": "mó fǎng",
"一模一样": "yī mú yī yàng",
"模样": "mú yàng",
"_量_liàng_vs_liáng": "quantity (liàng) vs measure-action (liáng)",
"数量": "shù liàng",
"质量": "zhì liàng",
"容量": "róng liàng",
"测量": "cè liáng",
"衡量": "héng liáng",
"_觉_jué_vs_jiào": "perceive (jué) vs sleep (jiào)",
"感觉": "gǎn jué",
"觉得": "jué de",
"视觉": "shì jué",
"听觉": "tīng jué",
"直觉": "zhí jué",
"自觉": "zì jué",
"错觉": "cuò jué",
"睡觉": "shuì jiào",
"_应_yīng_vs_yìng": "should (yīng) vs respond (yìng)",
"应该": "yīng gāi",
"应当": "yīng dāng",
"答应": "dā yìng",
"反应": "fǎn yìng",
"适应": "shì yìng",
"响应": "xiǎng yìng",
"对应": "duì yìng",
"_干_gān_vs_gàn": "dry/relate (gān) vs do/cadre (gàn)",
"干净": "gān jìng",
"相干": "xiāng gān",
"干燥": "gān zào",
"干杯": "gān bēi",
"干部": "gàn bù",
"干劲": "gàn jìn",
"干活": "gàn huó",
"_转_zhuǎn_vs_zhuàn": "redirect (zhuǎn) vs rotate (zhuàn)",
"转换": "zhuǎn huàn",
"转移": "zhuǎn yí",
"转告": "zhuǎn gào",
"转账": "zhuǎn zhàng",
"转向": "zhuǎn xiàng",
"转变": "zhuǎn biàn",
"转发": "zhuǎn fā",
"转载": "zhuǎn zài",
"旋转": "xuán zhuàn",
"公转": "gōng zhuàn",
"自转": "zì zhuàn",
"_划_huá_vs_huà": "row/scratch (huá) vs plan/divide (huà)",
"计划": "jì huà",
"规划": "guī huà",
"策划": "cè huà",
"划分": "huà fēn",
"划算": "huá suàn",
"_数_shù_vs_shǔ": "number (shù) vs count-action (shǔ)",
"数字": "shù zì",
"数据": "shù jù",
"数据库": "shù jù kù",
"数学": "shù xué",
"函数": "hán shù",
"参数": "cān shù",
"数组": "shù zǔ",
"次数": "cì shù",
"数数": "shǔ shù",
"_当_dāng_vs_dàng": "ought/serve-as (dāng) vs proper/be-fooled (dàng)",
"当然": "dāng rán",
"当时": "dāng shí",
"当下": "dāng xià",
"相当": "xiāng dāng",
"适当": "shì dàng",
"上当": "shàng dàng",
"妥当": "tuǒ dàng",
"_弹_tán_vs_dàn": "play/elastic (tán) vs bullet (dàn)",
"弹性": "tán xìng",
"弹簧": "tán huáng",
"弹琴": "tán qín",
"反弹": "fǎn tán",
"子弹": "zǐ dàn",
"导弹": "dǎo dàn",
"_假_jiǎ_vs_jià": "false/suppose (jiǎ) vs vacation (jià)",
"假设": "jiǎ shè",
"假如": "jiǎ rú",
"假装": "jiǎ zhuāng",
"假象": "jiǎ xiàng",
"假冒": "jiǎ mào",
"放假": "fàng jià",
"假期": "jià qī",
"暑假": "shǔ jià",
"寒假": "hán jià",
"请假": "qǐng jià",
"_倒_dǎo_vs_dào": "fall/topple (dǎo) vs reverse (dào)",
"倒下": "dǎo xià",
"倒闭": "dǎo bì",
"颠倒": "diān dǎo",
"摔倒": "shuāi dǎo",
"倒车": "dào chē",
"倒影": "dào yǐng",
"倒是": "dào shì",
"倒退": "dào tuì",
"倒计时": "dào jì shí",
"_几_jī_vs_jǐ": "almost/small-table (jī) vs how-many (jǐ)",
"几乎": "jī hū",
"茶几": "chá jī",
"几个": "jǐ gè",
"几次": "jǐ cì",
"几年": "jǐ nián",
"几位": "jǐ wèi",
"_卡_kǎ_vs_qiǎ": "card (kǎ) vs stuck/checkpoint (qiǎ)",
"卡片": "kǎ piàn",
"打卡": "dǎ kǎ",
"信用卡": "xìn yòng kǎ",
"显卡": "xiǎn kǎ",
"网卡": "wǎng kǎ",
"卡顿": "kǎ dùn",
"卡住": "qiǎ zhù",
"关卡": "guān qiǎ",
"_brands_en_to_zh": "English-spelled brand names with established Chinese pronunciations. Subtitle keeps original spelling, audio reads in Chinese.",
"Qwen": "qiān wèn",
"Bilibili": "bì lì bì lì"
}
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://video-podcast-maker/prefs_schema.json",
"title": "User Preferences Schema",
"description": "Schema for learnable user preferences in video-podcast-maker",
"type": "object",
"required": ["version", "updated_at"],
"properties": {
"version": {
"type": "string",
"description": "Schema version"
},
"updated_at": {
"type": "string",
"format": "date-time",
"description": "Last update timestamp"
},
"global": {
"type": "object",
"properties": {
"platform": {
"type": "string",
"enum": ["bilibili", "youtube", "xiaohongshu", "douyin", "weixin-channels"],
"default": "bilibili",
"description": "Target video platform"
},
"language": {
"type": "string",
"enum": ["zh-CN", "en-US"],
"default": "zh-CN",
"description": "Content language (affects script template, TTS voice, subtitle font)"
},
"cta": {
"type": "object",
"description": "Call-to-action configuration for outro",
"properties": {
"type": {
"type": "string",
"enum": ["auto", "animation", "text", "custom"],
"default": "auto",
"description": "CTA mode: auto (platform default), animation, text with icons, or custom"
},
"text": {
"type": ["string", "null"],
"default": null,
"description": "Custom CTA text. Null = use platform+language default"
},
"animation": {
"type": ["string", "null"],
"default": null,
"description": "Custom animation file path in assets/. Null = platform default"
}
}
},
"subtitle": {
"type": "object",
"description": "Subtitle burning preferences for FFmpeg",
"properties": {
"enabled": {
"type": "boolean",
"default": true,
"description": "Whether to burn subtitles into final video"
},
"fontName": {
"type": "string",
"default": "auto",
"description": "Subtitle font. 'auto' resolves by language: zh-CN=PingFang SC, en-US=Arial"
},
"fontSize": {
"type": "number",
"minimum": 10,
"maximum": 40,
"default": 20,
"description": "Subtitle font size (for 4K output)"
},
"primaryColor": {
"type": "string",
"default": "&H00333333",
"description": "Subtitle text color (ASS format BBGGRR)"
},
"outlineColor": {
"type": "string",
"default": "&H00FFFFFF",
"description": "Subtitle outline color (ASS format BBGGRR)"
},
"bold": {
"type": "boolean",
"default": false
},
"outline": {
"type": "number",
"minimum": 0,
"maximum": 5,
"default": 2,
"description": "Outline thickness in pixels"
}
}
},
"visual": {
"type": "object",
"properties": {
"theme": {
"type": "string",
"enum": ["light", "dark", "auto"],
"default": "light"
},
"primaryColor": {
"type": "string",
"description": "Primary brand color (hex or CSS color)"
},
"backgroundColor": {
"type": "string",
"description": "Background color (hex or CSS color)"
},
"accentColor": {
"type": "string",
"description": "Accent color for highlights (hex or CSS color)"
},
"preferredComponents": {
"type": "array",
"items": { "type": "string" },
"description": "List of preferred visual components"
},
"titlePosition": {
"type": "string",
"enum": ["top-center", "top-left", "center"],
"default": "top-center"
},
"progressBar": {
"oneOf": [
{ "type": "boolean" },
{
"type": "object",
"properties": {
"enabled": { "type": "boolean", "default": true },
"height": { "type": "number", "minimum": 2, "maximum": 20, "default": 6 },
"fontSize": { "type": "number", "minimum": 10, "maximum": 30, "default": 18 },
"activeColor": { "type": "string", "default": "auto", "description": "'auto' = follows primaryColor" },
"position": { "type": "string", "enum": ["bottom"], "default": "bottom" }
}
}
],
"default": true,
"description": "Chapter progress bar config (boolean for back-compat, or object)"
},
"typography": {
"type": "object",
"properties": {
"heroTitle": {
"type": "number",
"minimum": 72,
"default": 96
},
"sectionTitle": {
"type": "number",
"minimum": 60,
"default": 72
},
"cardTitle": {
"type": "number",
"minimum": 24,
"default": 36
},
"bodyText": {
"type": "number",
"minimum": 24,
"default": 28
},
"caption": {
"type": "number",
"minimum": 18,
"default": 22
},
"scalePreference": {
"type": "number",
"minimum": 0.5,
"maximum": 2.0,
"default": 1.0
}
}
},
"layout": {
"type": "object",
"properties": {
"textAlign": {
"type": "string",
"enum": ["center", "left"],
"default": "center"
},
"contentWidth": {
"type": "number",
"minimum": 0.5,
"maximum": 1.0,
"default": 0.85,
"description": "Content width as fraction of screen"
},
"cardPadding": {
"type": "number",
"minimum": 0,
"default": 40
}
}
},
"spacing": {
"type": "object",
"properties": {
"sectionPadding": {
"type": "number",
"minimum": 0,
"default": 60
},
"itemGap": {
"type": "number",
"minimum": 0,
"default": 24
},
"marginBottom": {
"type": "number",
"minimum": 0,
"default": 100,
"description": "Bottom margin reserved for subtitles"
}
}
}
}
},
"tts": {
"type": "object",
"properties": {
"backend": {
"type": "string",
"enum": ["azure", "doubao", "cosyvoice", "edge", "elevenlabs", "google", "openai"],
"default": "edge"
},
"rate": {
"type": "string",
"pattern": "^[+-]?\\d+%$",
"default": "+0%",
"description": "Speech rate adjustment (e.g. +5%, -10%)"
},
"voices": {
"type": "object",
"description": "Per-backend voice identifiers",
"properties": {
"azure": {
"type": "string",
"default": "zh-CN-XiaoxiaoNeural"
},
"edge": {
"type": "string",
"default": "zh-CN-XiaoxiaoNeural"
},
"doubao": {
"type": "string",
"default": "BV001_streaming"
},
"cosyvoice": {
"type": "string",
"default": "longxiaochun"
},
"elevenlabs": {
"type": "string",
"default": "21m00Tcm4TlvDq8ikWAM",
"description": "ElevenLabs voice ID (default: Rachel)"
},
"google": {
"type": "string",
"default": "en-US-Neural2-F",
"description": "Google Cloud TTS voice name"
},
"openai": {
"type": "string",
"default": "alloy",
"description": "OpenAI TTS voice name"
}
}
}
}
},
"bgm": {
"type": "object",
"description": "Background music preferences",
"properties": {
"volume": {
"type": "number",
"minimum": 0,
"maximum": 0.3,
"default": 0.10,
"description": "BGM volume (0-0.3, used in FFmpeg mixing and Remotion bgmVolume). Tuned for narration boost=1.5 + amix normalize=0; gives ~18 dB BGM-vs-narration headroom."
},
"track": {
"type": "string",
"default": "perfect-beauty",
"description": "Default BGM track name (key in tracks object)"
},
"tracks": {
"type": "object",
"description": "Named BGM tracks (name → filename in assets/)",
"additionalProperties": {
"type": "string"
},
"default": {
"perfect-beauty": "perfect-beauty-191271.mp3",
"calm-piano": "snow-stevekaldes-piano-397491.mp3"
}
}
}
},
"content": {
"type": "object",
"properties": {
"tone": {
"type": "string",
"enum": ["professional", "casual", "energetic"],
"default": "professional"
},
"verbosity": {
"type": "string",
"enum": ["concise", "balanced", "detailed"],
"default": "balanced"
},
"sectionCount": {
"type": "number",
"minimum": 3,
"maximum": 15,
"default": 6
},
"outroStyle": {
"type": "string",
"enum": ["standard", "minimal"],
"default": "standard"
},
"chapters": {
"type": "boolean",
"default": true,
"description": "Include chapter timestamps in publish_info.md"
}
}
}
}
},
"topic_patterns": {
"type": "object",
"description": "Topic-specific preference overrides",
"additionalProperties": {
"type": "object",
"properties": {
"visual": {
"$ref": "#/properties/global/properties/visual"
},
"tts": {
"$ref": "#/properties/global/properties/tts"
},
"content": {
"$ref": "#/properties/global/properties/content"
}
}
}
},
"style_profiles": {
"type": "object",
"description": "User-defined style profiles, each linking design preferences to reference videos",
"additionalProperties": {
"type": "object",
"required": ["description", "props_override", "references", "created_at", "updated_at"],
"properties": {
"description": {
"type": "string",
"description": "Human-readable description of this style"
},
"props_override": {
"type": "object",
"description": "Remotion videoSchema prop overrides (primaryColor, backgroundColor, etc.)",
"additionalProperties": true
},
"preferred_layouts": {
"type": "array",
"items": { "type": "string" },
"description": "Preferred section layout component names (CenteredShowcase, SplitLayout, etc.)"
},
"preferred_backgrounds": {
"type": "array",
"items": {
"type": "string",
"enum": ["gradient", "shapes", "grid", "clean"]
}
},
"animation_feel": {
"type": "string",
"enum": ["gentle", "snappy", "bouncy"]
},
"density": {
"type": "string",
"enum": ["spacious", "balanced", "dense"]
},
"references": {
"type": "array",
"items": { "type": "string" },
"description": "Reference IDs linked to this profile"
},
"created_at": {
"type": "string",
"format": "date"
},
"updated_at": {
"type": "string",
"format": "date"
}
}
}
},
"design_references": {
"type": "object",
"description": "Index of analyzed design references (data stored in design_references/ directory)",
"additionalProperties": {
"type": "object",
"required": ["path", "analyzed_at"],
"properties": {
"path": {
"type": "string",
"description": "Relative path to reference directory"
},
"title": {
"type": "string",
"description": "Title of the reference video"
},
"source_url": {
"type": "string",
"description": "Original URL if applicable"
},
"analyzed_at": {
"type": "string",
"format": "date"
},
"tags": {
"type": "array",
"items": { "type": "string" }
}
}
}
},
"learning_history": {
"type": "array",
"items": {
"type": "object",
"required": ["date", "source", "change"],
"properties": {
"date": {
"type": "string",
"format": "date-time"
},
"source": {
"type": "string",
"enum": ["explicit", "implicit", "feedback"],
"description": "How the preference was learned"
},
"change": {
"type": "object",
"required": ["path", "to"],
"properties": {
"path": {
"type": "string",
"description": "JSON path to the changed preference"
},
"from": {
"description": "Previous value (null if new)"
},
"to": {
"description": "New value"
}
}
},
"context": {
"type": "string",
"description": "Optional context about why change was made"
}
}
}
}
}
}
Azure TTS Pitfalls
When to load: When choosing voice/style for Azure backend, or when debugging hoarse / missing / glitchy audio. Skip for other backends.
The Azure neural-TTS engine is excellent in the common path but has several deterministic failure modes that have wasted hours of iteration. This file documents the known traps and how to avoid them.
---
Voice selection
Default: zh-CN-XiaoxiaoNeural (standard)
Use this for content that is mostly Chinese with rare English abbreviations (AI, ML, GPT, CLI, API, etc.). Chinese listeners read these abbreviations as letter-by-letter Chinese phonetics ("ei-ai", "em-el") in normal conversation — the standard voice produces exactly that, with no language switch and no artifacts.
Use zh-CN-XiaoxiaoMultilingualNeural ONLY when:
- The script contains substantial English passages (sentences, paragraphs, dialogue)
- Heavy technical content with proper nouns that genuinely need English pronunciation (e.g. "Visual Studio Code", "Final Cut Pro", spoken URLs)
- Mixed bilingual narration where English flow matters
Multilingual voice known issues:
| Issue | Manifestation | Workaround |
|---|---|---|
| Vocoder artifact at lang switch | Hoarse / strained sound when going Chinese-tone → English-letter → Chinese-consonant. E.g. "观点是,AI让答案" → "AI" sounds glitched | Switch to standard voice. Or rewrite phrase to avoid bare English token after Chinese tonal particle. |
| SAPI phoneme tags are silently dropped | <phoneme alphabet="sapi" ph="ka 3 zhu 4">卡住</phoneme> — surrounding text gets eaten, only tag content survives | Use standard voice (better SAPI support). Or remove inline phoneme markers — Azure usually pronounces common multi-character words correctly without override. |
| Style support is inconsistent | style="serious" / "newscast" may produce strained/hoarse output | Use empty TTS_STYLE="" to disable express-as wrapper; or stick to gentle / cheerful. |
| Word boundary timing | result.audio_duration may under-report when <break> / <phoneme> present | Reconciled automatically by reconcile_timing_with_wav in srt.py |
Picking voice from content
The tts/voice_advisor.py module analyses your script and prints a recommendation at TTS startup. Heed its warnings. Override via AZURE_TTS_VOICE env var if you disagree.
---
SSML pitfalls
<phoneme> for Chinese with multilingual voice → text loss
Symptom: A line like "你没有被细节卡住" plays as only "卡住" — the preceding "你没有被细节" is missing from the audio.
Cause: Inline 卡住[kǎ zhù] becomes <phoneme alphabet="sapi" ph="ka 3 zhu 4">卡住</phoneme>. Multilingual voice's SSML parser doesn't fully support SAPI Chinese phonemes; it silently drops the surrounding text in the same prosody block.
Fix: 1. Switch to standard zh-CN-XiaoxiaoNeural (better SAPI support), OR 2. Remove the [pinyin] annotation — Azure usually gets common compounds (重新, 卡住, 好的, 还是) right by default.
<break> and <phoneme> skew duration accounting
Symptom: After regen, timing.json total is ~250s but the actual WAV is ~258s. Last sections get truncated in Remotion render.
Cause: Azure's result.audio_duration may exclude <break> time and under-report <phoneme> duration.
Fix: Already automated. generate_tts.py calls reconcile_timing_with_wav after concat, ffprobing the actual file and rescaling sections proportionally if drift > 0.5s.
<say-as interpret-as="characters"> nested in <lang> — undefined behaviour
Symptom: <say-as> appears to have no effect or produces unexpected output.
Cause: Under Multilingual voice, mark_english_terms runs in aggressive mode and wraps single English words in <lang xml:lang="en-US">. Pre-writing <say-as> around the same word produces <say-as><lang>Word</lang></say-as> — Azure picks one or the other unpredictably.
Fix: Avoid <say-as> for English tokens — let voice selection (above) handle it.
---
How mark_english_terms chooses what to wrap
The function in scripts/tts/ssml.py runs in one of three modes, picked by wrap_mode_for(backend, voice) in the same file:
| Backend × voice | Mode | What gets wrapped |
|---|---|---|
azure + standard zh-CN-XiaoxiaoNeural | multi_word_only | Brand / proper-noun phrases only (Visual Studio Code, Andrew Ng, Apple Intelligence, …). Bare abbreviations (AI, ML, GPT, CLI, API) are left alone — standard voice reads them as natural Chinese letter pronunciations. |
azure + zh-CN-XiaoxiaoMultilingualNeural | aggressive | Brand phrases + single English words ≥5 chars with at least one lowercase letter and not in voice_advisor.COMMON_ABBREVS (e.g. transformer, embedding, Python). Bare abbreviations still skipped. |
edge / cosyvoice / doubao / elevenlabs / openai / google | off | Nothing. These backends pass plain text and would either escape or speak <lang> tags aloud. |
The matrix is driven by BACKENDS[name]['supports_ssml'] in scripts/tts/backends/__init__.py — change one place and both wrap_mode_for and the phoneme-not-supported warning in generate_tts.py follow.
To force a specific brand phrase to be wrapped under standard voice, add it to BRAND_PHRASES in scripts/tts/ssml.py. To force a one-off proper-noun pronunciation in a single script, hand-write <lang xml:lang="en-US">…</lang> directly in podcast.txt; the placeholder mechanism preserves it through mark_english_terms.
---
Style support matrix
These styles are reliably supported on zh-CN-XiaoxiaoNeural:
| Style | Use for | Note |
|---|---|---|
gentle (default) | General narration | Safe default |
cheerful | Light/positive tone | Energetic |
serious | News, professional | Punchy |
newscast | Reporting style | Steady cadence |
calm | Slow-paced explainer | Soothing |
chat | Casual conversation | Natural pause |
(empty "") | Disable wrapper | When any style produces artifacts |
For Multilingual voice, restrict to gentle or "". Other styles are inconsistent.
Set per-run via env: TTS_STYLE=cheerful python3 generate_tts.py ... or persist in user_prefs.json → global.tts.azure_style.
---
Quick triage checklist
Hearing weird audio at a specific timestamp?
1. Locate the bad word in `podcast_audio.srt` (timestamp ranges → words said when) 2. Check the surrounding context — Chinese tonal particle + bare English letter is the #1 hoarse trigger on Multilingual voice 3. Run voice advisor — if it suggests standard voice and you're on Multilingual, switch 4. Inspect inline phoneme markers near the bad word — remove if any 5. Try `TTS_STYLE=""` — disable express-as wrapper as a low-risk first try 6. Last resort: substitute that one Chinese homophone for the bare English token (e.g. AI → 诶艾). Surgical, audible difference is zero for Chinese listeners.
Video Podcast Maker — Design Guide
When to load: Load this file when working on Step 9 (Remotion composition) or when the user asks about visual design.
Contents
- Visual Design Minimums (MUST follow) — text sizes, spacing, space utilization, visual richness
- Design Philosophy
- Animation Safety (MUST follow)
- Quality Checklists (MUST follow)
- Visual Design Reference (recommended)
- Animated Backgrounds (SHOULD use)
- Section Layout Presets (SHOULD use)
- Animation Best Practices (SHOULD follow)
---
Visual Design Minimums (MUST follow)
Hard constraints for 1080p design space — prevents small text and empty layouts:
| Constraint | Minimum |
|---|---|
| Any text | ≥ 24px |
| Hero title | ≥ 84px |
| Section title | ≥ 72px |
| Card title | ≥ 40px |
| Card / body text | ≥ 32px |
| Icon size | ≥ 56px |
| Section padding | ≥ 40px |
| Card padding | ≥ 40px 48px |
| Card border-radius | ≥ 24px |
| Grid gap | ≥ 28px |
Space Utilization
| Rule | Requirement |
|---|---|
| Content width | Cards/grids must occupy ≥85% available width (maxWidth: 1500-1700px) |
| Vertical centering | All sections must use justifyContent: 'center' |
| Grid vs Flex | Prefer grid over flex wrap for multi-card layouts |
| Card fill | Cards should fill available space, no large blank areas |
Visual Richness
| Element | Requirement |
|---|---|
| Card borders | Colored border (≥3px) or colored left border (≥6px) |
| Shadows | boxShadow: '0 8px 24px rgba(color, 0.15)' |
| Color coding | Parallel elements use different theme colors |
| Gradients | Prefer gradient over solid color for card backgrounds |
| Icons | Each card/point needs an icon, size ≥56px |
---
Design Philosophy
Templates (templates/) are starting points, not blueprints. The agent SHOULD customize visual design per video topic:
- Color palette: match the subject (tech → cool blues/grays, food → warm tones, finance → dark/gold)
- Section layouts: create new component arrangements, don't repeat the same layout
- Visual variety: vary backgrounds, card styles, emphasis techniques across sections
- Typography: adjust sizes and weights for clear visual hierarchy per content density
- Animations: use entrance animations and transitions that fit the video's energy
Keep consistent: Technical Rules (4K, safe zones, min sizes), component imports, timing.json-driven timing.
Vary freely: colors, gradients, backgrounds, layout composition, card styles, icon choices, spacing, animation timing, section visual identity.
Theme-based background rules
When user_prefs.visual.theme is "light":
- All section backgrounds MUST be white (`#ffffff`). Do NOT use dark backgrounds, colored backgrounds, or alternate grays.
- Use subtle radial/linear gradients over white (
${color}06-${color}10) for visual variety between sections. - All text must use dark colors (
#1a1a1aor theme colors). Never use white/light text. - Tech cards, stat cards, etc. should use tinted backgrounds (
${color}08) instead of dark card backgrounds.
When user_prefs.visual.theme is "dark":
- Section backgrounds use dark colors (
#0f172a,#1a1a2e, etc.) - Text uses white/light colors.
---
Animation Safety (MUST follow)
Hard limits to prevent strobing, flicker, and visual fatigue. These apply to every composition regardless of style.
| Rule | Requirement |
|---|---|
| Full-screen brightness change | opacity of any full-bleed white/black overlay must NOT exceed 0.30 |
| Snap duration | No light↔dark snap may complete in fewer than 10 frames (~0.33s @ 30fps) |
| Section transition flash | Forbidden by default. Use color pulse (≤0.15 opacity) or simple cut instead. |
| Per-beat reveal flash | If used (e.g. "kinetic flash" effect), peak opacity ≤ 0.15 and decay ≥ 12 frames |
| Strobe-frequency animation | Forbidden. Any flicker faster than 3Hz risks accessibility issues (photosensitive viewers) |
| Background pulse | Color/glow pulses on background layers ≤ 0.15 opacity, period ≥ 2s |
| Text colour inversion | Don't invert text colour on a flash beat — it disappears against the dark BG once flash decays |
Why: full-screen flashes (even at 0.85 opacity for 6 frames) feel like camera-flash strobes when watched repeatedly during edit/preview. Most kinetic-typography references use type motion for punch, not background strobes. The visual energy should come from character pop / marker reveal / scale animations, not from the canvas blinking.
Quality Checklists (MUST follow)
Per-Section Checklist
| # | Check | Requirement |
|---|---|---|
| 1 | Space utilization | Content ≥85% available width, no large blank areas |
| 2 | Visual depth | Shadow + colored border/gradient, at least 2 visual layers |
| 3 | Color coding | Parallel cards use different theme colors (border, title, icon) |
| 4 | Typography scale | Title ≥72px, card title ≥40px, body ≥32px, icon ≥56px |
| 5 | Complete animation | All elements have entrance animation, list items have stagger delay |
| 6 | Vertical centering | Use justifyContent: 'center' |
Video-Level Checklist (before render)
| # | Check | Requirement |
|---|---|---|
| 1 | Layout variety | ≥3 different layout types (centered, grid, split, timeline, etc.) |
| 2 | Background alternation | No 2 consecutive sections share the same background |
| 3 | Unified color scheme | Primary/secondary/accent used consistently |
| 4 | Thumbnail readability | Title ≥120px, centered, legible at ~300px thumbnail width |
| 5 | Hero impact | Large text + decorative elements or gradient |
Remotion Thumbnail Design
Thumbnails render at ~300px in feeds. Only bold, oversized elements survive at that scale.
Hard rules (MUST follow):
| Rule | Requirement |
|---|---|
| Layout | Content centered both horizontally and vertically |
| Title | ≥120px bold. The dominant visual element — as large as the text length allows |
| Icons | ≥120px. Serve as visual anchors alongside text |
| Coverage | Text + icons should fill most of the canvas. Minimize empty space. |
| Readability | Must be legible at 300px width — use text-stroke, text-shadow, or contrasting overlay as needed |
Design guidance (adapt per topic):
- Prefer 2-3 lines of text (title + subtitle or hook) over a single line — more text = more information density
- Use high-saturation colors that pop in feeds; avoid muted tones
- Consider a short hook phrase ("Why It Matters", "Top 5") to boost curiosity — as badge, subtitle, or integrated into the title
- Background should support the text, not compete with it (gradient, blurred image, solid color all work)
- When in doubt, scale text and icons up rather than down
TTS Quality Guidance
| Technique | How |
|---|---|
| Section pauses | Empty line before each [SECTION:xxx] for natural breathing room |
| Pacing variation | Slower intro/outro (TTS_RATE="+0%"), normal middle (TTS_RATE="+5%") |
| Key sentence emphasis | SSML <emphasis> tags on important sentences (Azure supports this) |
---
Visual Design Reference (recommended)
Production-verified sizes as recommended reference. The agent may adjust freely but MUST NOT go below Minimums.
Typography Scale (1080p design space)
| Element | Recommended Size | Weight | When to Use |
|---|---|---|---|
| Hero Title | 84-96px | 800 | Opening section, brand moment |
| Section Title | 72-80px | 700-800 | Each section's main heading |
| Large Emphasis | 48-72px | 600-700 | Key statements, conclusions, quotes |
| Subtitle / Description | 36-44px | 500-600 | Under section titles, subheadings |
| Card Title | 40-48px | 700 | Feature cards, list group headers |
| Body Text | 32-40px | 500-600 | Paragraphs, list items, descriptions |
| Tags / Pills | 28-36px | 600 | Labels, badges, categories |
| Icons | 56-80px | — | Card icons, decorative elements |
Icon Selection Guide
The full Lucide icon library (1500+ icons) is available. Use kebab-case names (e.g., "shield-check", "brain-circuit"). Browse https://lucide.dev/icons/ to find the best match.
Semantic mapping — pick icons by concept, not by guessing:
| Concept | Recommended Icon | Alternatives |
|---|---|---|
| AI / Intelligence | brain | brain-circuit, sparkles, bot |
| Speed / Performance | zap | gauge, rocket, timer |
| Security / Privacy | shield | shield-check, lock, key-round |
| Data / Analytics | bar-chart-3 | pie-chart, trending-up, chart-line |
| Money / Cost | dollar-sign | wallet, coins, banknote |
| Team / Collaboration | users | users-round, handshake, message-circle |
| Cloud / Infra | cloud | server, database, hard-drive |
| Code / Dev | code | terminal, file-code, braces |
| Success / Complete | check-circle | circle-check, badge-check, trophy |
| Warning / Risk | alert-triangle | shield-alert, alert-circle, ban |
| Innovation / New | lightbulb | sparkles, wand, lamp |
| Growth / Increase | trending-up | arrow-up-right, chart-no-axes-combined, sprout |
| Time / Schedule | clock | calendar, timer, hourglass |
| Global / World | globe | earth, map, languages |
| Settings / Config | settings | sliders-horizontal, wrench, cog |
| Communication | message-circle | mail, phone, megaphone |
| Education / Learn | graduation-cap | book-open, school, notebook-pen |
| Health / Medical | heart-pulse | activity, stethoscope, pill |
| Mobile / App | smartphone | tablet, app-window, monitor |
| Integration / Connect | plug | cable, link, puzzle |
| Search / Discover | search | scan, radar, compass |
| Storage / Save | database | hard-drive, archive, save |
| Automation / Workflow | workflow | repeat, git-branch, route |
| Creative / Design | palette | brush, pen-tool, figma |
| Video / Media | video | play-circle, clapperboard, film |
Rules:
- Always use semantic names from this table when the concept matches
- For concepts not listed, browse https://lucide.dev/icons/ and pick the closest match
- Prefer specific icons over generic ones (e.g.,
shield-checkovercheckfor "security verified") - Each card/section in a group should use a different icon — never repeat icons in parallel items
Brand Logos for AI / LLM Topics
When the video topic involves specific AI companies, LLM models, or developer tools, use @lobehub/icons for official brand logos (OpenAI, Anthropic, Google, Meta, Mistral, Cohere, Hugging Face, LangChain, etc.). Lucide covers generic concepts; lobehub covers the brand mark itself.
Install (in the Remotion project root, not in `videos/{name}/`):
npm i @lobehub/iconsUpstream agent guidance: read https://lobehub.com/icons/skill.md for the current import pattern and component API. The package evolves independently — always check the upstream skill doc rather than relying on cached examples here.
When to use which:
- Concept icon (shield, brain, zap) → Lucide
- Brand mark (the OpenAI logo, the Claude logo) → lobehub
- Never use both redundantly for the same item (e.g., a card titled "OpenAI" needs only the lobehub OpenAI mark, not lobehub + Lucide
sparkles)
Layout Patterns (recommended)
| Pattern | Recommended |
|---|---|
| Card | borderRadius: 24-32px, padding: 48px 56px, colored border (3px) + shadow |
| Section Padding | 40px 60px content, 40px 80px hero |
| Grid Gap | 28-40px |
| Content Max Width | 1500-1700px for grids, 1400px for centered blocks |
| Hero / Impact | Full viewport centered with decorative icon |
| 2-column cards | display: grid; gridTemplateColumns: 1fr 1fr with gap 36-56px |
| 4+ items | display: grid; gridTemplateColumns: repeat(4, 1fr) or repeat(2, 1fr) |
| Workflow/Steps | Unique color per step, circular number badge, colored border |
Color Coding Examples
| Scenario | Approach |
|---|---|
| Feature cards (4) | Green/purple/orange/pink — different theme color per card |
| Workflow steps | Blue→purple→green→orange→pink→cyan — gradient sequence |
| Intro cards (2) | Green vs purple — contrasting palettes |
| Outro buttons | Red/orange/yellow/pink — unique color per action |
Principle: Fill space, enlarge elements, enrich colors — no large blank areas.
---
Animated Backgrounds (SHOULD use)
Every section SHOULD include at least one animated background layer for depth. Available layers from AnimatedBackground.tsx:
| Component | Effect | Best For |
|---|---|---|
MovingGradient | Slowly rotating gradient overlay | Any section — adds subtle motion |
FloatingShapes | Drifting circles/rings | Hero, feature sections — adds depth |
GridPattern | Dot/line/cross grid overlay | Data sections, technical content |
GlowOrb | Pulsing color orb with blur | Hero, stat highlight — focal point |
AccentLine | Animated expanding line | Section dividers, emphasis |
Rules:
- Use 1-2 background layers per section (not all at once)
- Alternate between background styles across sections
- Keep opacity low (0.03-0.08) — backgrounds should be felt, not seen
FloatingShapescount: 3-5 shapes, more than 6 looks cluttered
---
Section Layout Presets (SHOULD use)
Pre-built layouts from SectionLayouts.tsx — the agent SHOULD pick from these before creating custom layouts. Each includes animated backgrounds, proper spacing, and theme integration.
| Layout | Visual | Best For |
|---|---|---|
SplitLayout | Content left + visual right (or reversed) | Feature highlight, explanation + diagram |
StatHighlight | Full-bleed big number with glow orb | Key metric, percentage, impact statement |
ZigzagCards | Alternating left/right cards with colored borders | Feature list, pros/cons, step-by-step |
CenteredShowcase | Centered content with dual glow orbs + accent lines | Key quote, thesis, conclusion |
MetricsRow | Dashboard-style stat cards in grid | Comparison numbers, KPIs, benchmarks |
StepProgress | Numbered steps with active highlight | Workflow, tutorial steps, process |
Plus existing components: ComparisonCard, Timeline, CodeBlock, QuoteBlock, FeatureGrid, DataBar, StatCounter, FlowChart, IconCard, MediaSection, DiagramReveal
SVG Draw-On Animations
FlowChart and Timeline now use animated SVG paths with draw-on effects via @remotion/paths. The DiagramReveal component provides general-purpose node+edge diagram animation.
| Component | Draw-On Feature | Use Case |
|---|---|---|
FlowChart | Arrow connectors draw progressively between step cards | Process flows, pipelines |
Timeline | Node circles and vertical connectors draw on in sequence | History, milestones |
DiagramReveal | Arbitrary node+edge graphs with curve/elbow/straight edges | Architecture diagrams, mind maps, network graphs |
Animation hooks (animations.tsx):
useDrawOn(path, enabled, delay, duration, preset)— animate a single SVG pathuseStaggeredDrawOn(paths, enabled, delay, durationPerPath, interval, preset)— animate multiple paths sequentially
DiagramReveal props:
nodes:{ id, label, x, y, icon?, width?, height? }[]— positioned boxes in SVG viewBoxedges:{ from, to, label?, style? }[]— connections between nodes ("curve"|"straight"|"elbow")
Audio Waveform Visualization
AudioWaveform renders a real-time frequency visualization synced to the TTS narration audio. Makes the video feel alive rather than a slideshow with voiceover.
| Prop | Default | Description |
|---|---|---|
mode | "bars" | "bars" (spectrum bars), "wave" (filled waveform), "dots" (pulsing dots) |
position | "bottom" | "bottom", "top" (absolute positioned), or "inline" (flow layout) |
barCount | 32 | Number of frequency bars/samples |
height | 60 | Height in pixels (design space) |
opacity | 0.4 | Overall opacity — keep subtle (0.2-0.5) to avoid distraction |
Recommended usage: Add <AudioWaveform props={props} position="bottom" opacity={0.3} height={40} /> inside Video.tsx as a persistent overlay across all sections, or per-section for selective visibility.
Lottie Animations (After Effects)
LottieAnimation loads and plays After Effects animations exported as Lottie JSON. Unlocks the LottieFiles ecosystem (100,000+ free animations).
| Prop | Default | Description |
|---|---|---|
src | — | Path to JSON in --public-dir (via staticFile) or full CORS-enabled URL |
animationData | — | Pre-loaded JSON data (takes precedence over src) |
loop | false | Whether to loop the animation |
direction | "forward" | "forward" or "backward" |
playbackRate | 1 | Speed multiplier |
enableEntrance | false | Wrap in entrance fade-in animation |
Recommended usage: Download Lottie JSON files to videos/{name}/animations/, then reference with <LottieAnimation src="animations/brain.json" width={200} height={200} loop /> (resolved via --public-dir). Use for concept icons, character animations, decorative elements, or section transitions.
Layout Sequencing Rules (MUST follow)
The agent MUST vary layouts across sections. Follow these rules to prevent visual monotony:
| Rule | Requirement |
|---|---|
| No repeat | Never use the same layout preset for consecutive sections |
| Alternate density | Follow a high-density section (grid/cards) with a low-density one (showcase/stat) |
| Background variety | Consecutive sections must use different background layers |
| Hero → Impact → Detail → Breathe | Recommended rhythm: start bold, add detail, give a visual break, repeat |
Recommended sequencing for 7-section video:
1. Hero → CenteredShowcase or custom hero (high impact)
2. Overview → ZigzagCards or FeatureGrid (structured content)
3. Core concept → SplitLayout (explanation + visual)
4. Data/metrics → MetricsRow or StatHighlight (visual break with numbers)
5. Deep dive → StepProgress or Timeline (sequential content)
6. Summary → CenteredShowcase (return to high impact)
7. Outro → Custom outro with icons---
Animation Best Practices (SHOULD follow)
Use continuous animations for visual life
Every section should have at least subtle continuous motion:
- Background:
MovingGradientorFloatingShapes(always present) - Decorative:
GlowOrbwithusePulse(for emphasis sections) - Content: stagger entrance delays (6-10 frames between items)
Text reveal for key statements
Use useTextReveal or useCharReveal for:
- Hero title (character reveal, 2 frames/char)
- Key statistics or conclusions (word reveal, 4 frames/word)
- NOT for body text or descriptions (too slow, use regular entrance)
Animation timing guidelines
| Element | Entrance | Stagger | Exit |
|---|---|---|---|
| Title | 0 delay, "snappy" preset | — | — |
| Subtitle | 8 frame delay | — | — |
| Cards/items | 6-10 frame stagger | staggerDelay(i, 8) | — |
| Decorative | 15+ frame delay | — | — |
| Background | Continuous (no entrance) | — | — |
Design Learning
Extract visual design patterns from reference videos or images, store them in a searchable library, and apply them to new video compositions.
When to load: Only when the user runs /design-learn, provides a reference video/image, or asks to save/list/delete style profiles. Skip for normal video creation flow.---
Commands
# Learn from images (use your agent's image analysis capability to analyze design patterns)
python3 scripts/learn_design.py ./screenshot1.png ./screenshot2.png
# Learn from a local video (ffmpeg extracts frames automatically)
python3 scripts/learn_design.py ./reference.mp4
# Learn from a URL (NOT implemented — creates a placeholder ref_id only;
# add screenshots manually to <output-dir>/<ref_id>/frames/ afterwards)
python3 scripts/learn_design.py https://www.bilibili.com/video/BV1xx411c7mD
# Save with a named profile and tags (--tags is persisted in design_references
# index, --profile attaches the new ref_ids to the named style profile,
# auto-creating the profile if it doesn't exist yet)
python3 scripts/learn_design.py ./reference.mp4 --profile "tech-minimal" --tags "tech,minimal,dark"Reference Library Commands
All reference-library management goes through learn_design.py:
python3 scripts/learn_design.py --list # List all stored references (auto-cleans orphaned entries)
python3 scripts/learn_design.py --show <REF_ID> # Show report.json for a specific reference
# --delete is gated: without --yes it prints a preview and exits 3 (confirmation_required).
# Re-run with --yes after reviewing the preview to actually remove the entry and its files.
python3 scripts/learn_design.py --delete <REF_ID> # Preview only (no deletion)
python3 scripts/learn_design.py --delete <REF_ID> --yes # Confirm and delete<REF_ID> is the id printed by --list (e.g. bilibili-BV1xx411c7mD, or an md5-derived id for image sets).
Style Profile Management
Style profiles live under user_prefs.json → style_profiles. There is no dedicated CLI — manage them by editing the JSON directly, or ask the agent to do it for you in conversation:
- "list my style profiles" → agent reads and prints
user_prefs.style_profiles - "show profile tech-minimal" → agent prints that entry
- "delete profile tech-minimal" → agent removes that key and saves
- "create profile tech-minimal from this video" → agent extracts current visual props into a new profile entry
To attach references to a profile when learning, pass --profile <name> and --tags <csv> to learn_design.py.
---
Pre-Workflow Usage
When the user provides a reference video or image alongside a video creation request, extract design patterns before Step 1 and apply them as session overrides.
See references/workflow-script.md → Pre-workflow section for the full extraction flow.
Step 9 Integration
Before choosing visual design in Step 9, check for matching style profiles or reference library entries. Apply the best match as a starting point for Remotion composition props.
See references/workflow-production.md → Step 9 Style Profile Integration for the priority chain.
---
Troubleshooting
Common issues (ffmpeg not found, Playwright fails, orphaned references, style profile not applied) are covered in `references/troubleshooting.md` → "Design Learning Troubleshooting" section.
Video Podcast Maker — Troubleshooting & Reference
When to load: Load this file when encountering errors, when the user asks about preferences, or when the user asks about BGM options.
Contents
- Discovery (when you're not sure which script to run) —
cli.pyentry point + envelope error codes - Troubleshooting — TTS, FFmpeg, Remotion, encoding, audio-sync fixes
- Background Music Options
- Preference Commands
- Preference Learning
- Design Learning Troubleshooting
Discovery (when you're not sure which script to run)
The suite is reachable through one hierarchical entry point at scripts/cli.py:
python3 scripts/cli.py --help # 8 resources
python3 scripts/cli.py <resource> --help # actions for one resource
python3 scripts/cli.py <resource> <action> --help # full args (forwards to underlying script)
python3 scripts/cli.py schema # JSON list of all 14 methods
python3 scripts/cli.py schema <method> # typed parameter schema for one methodWhen a script fails with a structured envelope (most do — see --format json), the code field tells an agent how to recover: input_not_found, auth_missing_env, tool_missing, validation_failed, confirmation_required, ffmpeg_failed, backend_failed, internal_error. Direct invocations (python3 scripts/<name>.py ...) still work — the dispatcher is additive.
Troubleshooting
TTS: Azure API Key Error
Symptoms: Error: Authentication failed, HTTP 401 Unauthorized
Solution:
echo $AZURE_SPEECH_KEY
echo $AZURE_SPEECH_REGION
export AZURE_SPEECH_KEY="your-key-here"
export AZURE_SPEECH_REGION="eastasia"---
FFmpeg: BGM Mixing Issues
Symptoms: BGM too loud over voice, BGM ends abruptly
Solution:
# Basic mix (voice primary, BGM lowered)
ffmpeg -i voice.mp3 -i bgm.mp3 \
-filter_complex "[0:a]volume=1.0[voice];[1:a]volume=0.15[bgm];[voice][bgm]amix=inputs=2:duration=first" \
-ac 2 output.mp3
# With fade in/out
ffmpeg -i voice.mp3 -i bgm.mp3 \
-filter_complex "
[0:a]volume=1.0[voice];
[1:a]volume=0.15,afade=t=in:st=0:d=2,afade=t=out:st=58:d=2[bgm];
[voice][bgm]amix=inputs=2:duration=first
" output.mp3---
Remotion: Render Out of Memory
Symptoms: FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed, render crashes at ~50%
Solution:
# Reduce parallelism
npx remotion render ... --concurrency 1
# Or increase Node memory
NODE_OPTIONS="--max-old-space-size=8192" npx remotion render ...---
Remotion: Black Screen / No Content
Symptoms: Output video is all black or all white, no visual elements
Solution: 1. Verify timing.json exists in videos/{name}/ and has correct start_frame/duration_frames 2. Check composition ID matches: npx remotion render ... CompositionId must match Root.tsx registration 3. Ensure --public-dir videos/{name}/ is passed to all Remotion commands 4. Check browser console in npx remotion studio for JS errors
---
Remotion: Command Not Found
Symptoms: npx: command not found or remotion: not found
Solution:
# Ensure you're in the Remotion project directory
cd your-remotion-project
npm i # reinstall dependencies
npx remotion --version # verify---
timing.json: Parse Error
Symptoms: SyntaxError: Unexpected token, sections missing or misaligned
Solution:
# Validate JSON
python3 -c "import json; json.load(open('videos/{name}/timing.json'))"
# Check section names match podcast.txt [SECTION:xxx] markersCommon cause: section name in podcast.txt doesn't match the composition code.
---
SRT: Garbled Chinese Characters
Symptoms: Subtitles show ??? or mojibake
Solution:
# Check encoding
file videos/{name}/podcast_audio.srt
# Should show: UTF-8 Unicode text
# Convert if needed
iconv -f GBK -t UTF-8 videos/{name}/podcast_audio.srt > videos/{name}/podcast_audio_utf8.srt
mv videos/{name}/podcast_audio_utf8.srt videos/{name}/podcast_audio.srt---
Disk Space: 4K Render Fails
Symptoms: Render stops partway, No space left on device
Solution: 4K render needs ~10-20GB free space. Check with df -h . before rendering. Clean up old video outputs or use --scale 0.5 for 1080p.
---
Font Not Found (Linux)
Symptoms: Text renders in fallback font, Chinese characters show as boxes
Solution:
# Install Noto Sans SC
sudo apt install fonts-noto-cjk
# Or download PingFang SC manually---
Edge TTS: No Audio Output
Symptoms: Empty or zero-length WAV file, no error message
Solution: Edge TTS requires internet access (uses Microsoft's online TTS service). Check network connectivity. No API key needed.
---
Quick Checklists
Pre-render:
- [ ] All asset files exist
- [ ] timing.json format correct
- [ ] Audio duration matches timing
- [ ] Environment variables set
- [ ] Disk space sufficient (>20GB for 4K)
Post-render:
- [ ] Video duration correct
- [ ] Audio-video sync
- [ ] Subtitles display correctly
- [ ] No black/blank frames
---
Background Music Options
Included Tracks
Available at ${SKILL_DIR}/assets/:
| Track | Mood | Best For |
|---|---|---|
perfect-beauty-191271.mp3 | Upbeat, positive | Tech demos, product intros, tutorials |
snow-stevekaldes-piano-397491.mp3 | Calm piano | Reflective topics, analysis, comparisons |
Using Custom BGM
cp /path/to/my-bgm.mp3 videos/{name}/bgm.mp3If user says "use my own BGM" or provides a file path, skip the default BGM copy in Step 11.
Royalty-Free BGM Sources
| Source | URL | License |
|---|---|---|
| Pixabay Music | https://pixabay.com/music/ | Free, no attribution |
| Free Music Archive | https://freemusicarchive.org/ | CC licenses |
| Incompetech | https://incompetech.com/ | CC BY (attribution) |
| Uppbeat | https://uppbeat.io/ | Free tier available |
| Chosic | https://www.chosic.com/free-music/all/ | Various CC |
BGM Selection Guide
| Video Type | Recommended Mood | Volume |
|---|---|---|
| Tech/coding | Lo-fi, ambient | 0.03-0.05 |
| Product review | Upbeat, corporate | 0.05-0.08 |
| News/analysis | Neutral, minimal | 0.03-0.05 |
| Tutorial | Calm, steady | 0.04-0.06 |
| Lifestyle | Warm, acoustic | 0.05-0.08 |
Agent behavior: In auto mode, select most appropriate included track by topic type. In interactive mode, ask user.
---
Preference Commands
Users can manage preferences in conversation:
View Preferences
User says: "show preferences" / "显示偏好设置"
The agent outputs the current settings summary (visual, TTS, content, topic patterns, learning history count).
Reset Preferences
User says: "reset preferences" / "重置偏好"
cp ${SKILL_DIR}/user_prefs.template.json ${SKILL_DIR}/user_prefs.json
echo "✓ Preferences reset to defaults"Save Current Settings
User says: "save this as tech default" / "把这个设置保存为科技类默认"
The agent extracts current visual/TTS/content settings and updates topic_patterns.tech.
Manual Preference Setting
User says: "set speech rate to +10%" / "dark theme as default" / "title always 100px"
The agent directly updates the corresponding field in user_prefs.json.
Platform & Language Commands
| User Says | Action |
|---|---|
| "set platform youtube" | Update global.platform to "youtube" |
| "set platform bilibili" | Update global.platform to "bilibili" |
| "set platform xiaohongshu" | Update global.platform to "xiaohongshu" |
| "set platform douyin" | Update global.platform to "douyin" |
| "set platform weixin-channels" | Update global.platform to "weixin-channels" |
| "set language en-US" | Update global.language to "en-US" |
| "set language zh-CN" | Update global.language to "zh-CN" |
| "show platform" | Show current platform and language |
| "disable subtitles" | Set global.subtitle.enabled to false |
| "enable subtitles" | Set global.subtitle.enabled to true |
| "set subtitle font Arial" | Set global.subtitle.fontName to "Arial" |
| "set subtitle size 24" | Set global.subtitle.fontSize to 24 |
| "set CTA text" | Set global.cta.type to "text" |
| "set CTA animation" | Set global.cta.type to "animation" |
| "enable chapters" | Set global.content.chapters to true |
| "disable chapters" | Set global.content.chapters to false |
---
Preference Learning
Planned feature (not yet implemented). The schema supports learning_history records, but automatic detection of preference changes during Studio sessions is not yet coded. Currently, preferences are set manually via the commands above.Planned capabilities:
- Detect repeated style modifications during Studio preview
- Ask user whether to promote changes to global defaults
- Track learning history in
user_prefs.json
---
Design Learning Troubleshooting
"ffmpeg not found" when learning from video
Install ffmpeg: brew install ffmpeg (macOS) or use image input instead.
Playwright fails on Bilibili/YouTube
URL extraction is experimental. Fallback options: 1. Download the video and use: learn ./video.mp4 2. Take screenshots manually and use: learn ./screenshot1.png ./screenshot2.png
Vision analysis colors look wrong
Color values from image analysis are approximate. After reviewing the report:
- Adjust colors manually: edit report.json or override when creating the style profile
- Use a color picker tool on the screenshots for precise hex values
Style profile not applied
Check priority chain: style_profiles only override when explicitly specified by name. Verify: python3 scripts/learn_design.py --list shows the reference exists. Verify: user_prefs.json → style_profiles → your profile name exists with correct props_override.
Orphaned references (deleted directory but still in index)
Run references list — orphaned entries are auto-cleaned on list.
---
Doubao TTS: Phoneme System Not Supported
Symptoms: Inline phoneme markers 执行器[zhí xíng qì] and phonemes.json entries are ignored when using Doubao backend.
Explanation: Doubao TTS uses a plain-text HTTP API that does not support SSML or phoneme tags. The phoneme system (inline markers, project phonemes.json, global phonemes.json) only works with Azure TTS. CosyVoice and Edge TTS also do not apply phonemes.
Workaround: If pronunciation accuracy is critical, use Azure TTS (TTS_BACKEND=azure).
---
ElevenLabs / OpenAI / Google TTS Limitations
- No phoneme support: Inline markers
执行器[zhí xíng qì]andphonemes.jsonare ignored - OpenAI TTS has no word boundaries: Subtitle timing is approximate (evenly distributed across words). For precise subtitles, use Azure, Edge, or ElevenLabs
- Google Cloud TTS has no word boundaries: Subtitle timing is approximate (same as OpenAI). For precise subtitles, use Azure, Edge, or ElevenLabs
- Workaround: If subtitle precision is critical, use Azure TTS (
TTS_BACKEND=azure), Edge TTS (TTS_BACKEND=edge), or Google Cloud TTS is not recommended for subtitle-critical workflows
---
Doubao TTS: API Error Codes
Symptoms: Doubao API error code=XXXX
Common codes:
code != 3000: Non-success response. Check VOLCENGINE_APPID and VOLCENGINE_ACCESS_TOKEN.- HTTP 401/403: Invalid or expired access token. Regenerate at Volcengine Console.
- Timeout: Increase via
VOLCENGINE_TIMEOUT_SECenv var (default: 60s).
Video Podcast Maker — Workflow Phase 2: Production
When to load: Load after the narration script exists, or when the user asks about media, TTS, Remotion composition, 4K render, or BGM mixing.
>
Covers: Steps 5-11 (media collection → publish info draft → thumbnail → TTS → Remotion composition + Studio preview → 4K render → BGM mix).
>
Previous phase: See workflow-script.md for Pre-workflow + Startup + Steps 1-4.Next phase: See workflow-publish.md for Steps 12-15 (subtitles, publish info, cleanup, shorts).Contents
- Step 5: Collect Media Assets
- Step 6: Generate Publish Info (Part 1)
- Step 7: Generate Video Thumbnail
- Step 8: Generate TTS Audio — voice selection, SSML, chunk seams
- Step 9: Create Remotion Composition + Studio Preview
- Step 10: Render 4K Video
- Step 11: Mix with Background Music
---
Step 5: Collect Media Assets
Auto mode: Skip media collection (text-only animated sections). Proceed to Step 6. Interactive mode: Ask per-section media source (skip / local file / screenshot / web search / AI generated).
If user mentioned AI images, screenshots, or specific assets in initial request, collect those regardless of mode.
Save assets to videos/{name}/media/, generate media_manifest.json.
Available sources:
- Unsplash / Pexels / Pixabay — free images
- unDraw — open-source SVG illustrations
- Simple Icons — brand SVG icons
- Playwright — web screenshots
- imagen skill — AI-generated images
---
Step 6: Generate Publish Info (Part 1)
Based on podcast.txt, generate publish_info.md:
- Title (number + topic + hook)
- Tags (10, including product names / domain terms / trending tags)
- Description (100-200 chars)
---
Step 7: Generate Video Thumbnail
Auto mode: Generate Remotion thumbnails (16:9 + 4:3). Interactive mode: Ask user: Remotion-generated / AI (imagen skill) / both.
MUST generate both aspect ratios: 16:9 (playback page) and 4:3 (feed/activity), both required for horizontal video. (9:16 thumbnail is generated alongside the vertical render in Step 10/15 — not here.)
Thumbnail design rules (see references/design-guide.md for full spec):
- Centered layout, title ≥120px bold, icons ≥120px — as large as text length allows
- Text + icons should fill most of the canvas, minimize empty space
- Must be legible at 300px feed size — use text-stroke or contrast overlay
npx remotion still src/remotion/index.ts Thumbnail16x9 videos/{name}/thumbnail_remotion_16x9.png --public-dir videos/{name}/
npx remotion still src/remotion/index.ts Thumbnail4x3 videos/{name}/thumbnail_remotion_4x3.png --public-dir videos/{name}/xiaohongshu: Generate 3:4 thumbnail (replaces 4:3):
npx remotion still src/remotion/index.ts Thumbnail3x4 videos/{name}/thumbnail_remotion_3x4.png --public-dir videos/{name}/---
Step 8: Generate TTS Audio
Azure-specific gotchas: if you're using TTS_BACKEND=azure, load [azure-tts-pitfalls.md](azure-tts-pitfalls.md) before picking a voice or style — covers Multilingual-variant phoneme behavior, SSML pitfalls, the style support matrix, and a triage checklist for hoarse/missing/glitchy audio. Worth ~30 seconds of reading time and saves a re-render.Preference application: generate_tts.py reads user_prefs.tts.{backend, rate, voices.<backend>} automatically. No manual env extraction needed. Precedence for each setting: env var > user_prefs.json > hardcoded default. The script logs which source it picked at startup.
# Primary command — backend, rate, and voice all auto-resolved from user_prefs
python3 ${SKILL_DIR}/scripts/generate_tts.py --input videos/{name}/podcast.txt --output-dir videos/{name}
# Resume from breakpoint
python3 ${SKILL_DIR}/scripts/generate_tts.py --input videos/{name}/podcast.txt --output-dir videos/{name} --resume
# Dry run (estimate duration)
python3 ${SKILL_DIR}/scripts/generate_tts.py --input videos/{name}/podcast.txt --output-dir videos/{name} --dry-runOverride per-run (without editing user_prefs): TTS_BACKEND=edge TTS_RATE="+10%" python3 .... CLI --backend <name> also works and takes top priority.
Voice Selection by Language
The default path: edit user_prefs.json → global.tts.voices.<backend> once for the user's preferred language, then generate_tts.py picks it up automatically. Reference defaults if the user has not customized tts.voices:
| Language | Azure | Edge | Doubao | CosyVoice |
|---|---|---|---|---|
| zh-CN | zh-CN-XiaoxiaoNeural | zh-CN-XiaoxiaoNeural | BV001_streaming | longxiaochun |
| en-US | en-US-JennyNeural | en-US-JennyNeural | BV700_streaming | longlaoshu_v2 |
Manual override (one-off run, no prefs change) — set the per-backend env var:
# Per-backend env vars: AZURE_TTS_VOICE / EDGE_TTS_VOICE / VOLCENGINE_VOICE_TYPE / etc.
EDGE_TTS_VOICE="en-US-JennyNeural" python3 ${SKILL_DIR}/scripts/generate_tts.py --input videos/{name}/podcast.txt --output-dir videos/{name}Precedence: env var > user_prefs.json > hardcoded default. The script logs which source it picked at startup.
Phoneme Correction (SSML)
Three tiers (highest to lowest priority):
1. Inline annotation (highest) — in podcast.txt:
每个执行器[zhí xíng qì]都有自己的上下文窗口2. Project dictionary — in videos/{name}/phonemes.json:
{ "执行器": "zhí xíng qì", "重做": "chóng zuò" }3. Global dictionary — phonemes.json in skill root (shared across all projects)
Outputs: podcast_audio.wav, podcast_audio.srt, timing.json
timing.json `label` field: Each section gets a human-readable label from the first line of content (before first punctuation, max 10 chars). Example: [SECTION:hero] with "大家好,欢迎来到本期视频" → label: "大家好". Silent sections use section name as label.
---
Step 9: Create Remotion Composition + Studio Preview
The agent MUST read `references/design-guide.md` before this step.
Preference application: From user_prefs.visual override defaultVideoProps:
typography.*×scalePreference→ apply font scalingtheme: dark→ swap backgroundColor/textColorprimaryColor,accentColor→ direct override
All Remotion commands use --public-dir videos/{name}/ so assets are read directly from the video directory (no copying needed).
Style Profile Integration
Before choosing visual design, check in order: 1. Session-specified style profile? → Load user_prefs.json style_profiles[name], apply props_override 2. No profile? → Check design_references index for tag matches against detected topic 3. Found matches? → Suggest: "Your reference library has N references matching '{topic}'. Apply style '{profile_name}'?" 4. Nothing matches? → Fall back to global + topic_patterns (existing behavior)
Priority chain: Root.tsx defaults < global < topic_patterns[type] < style_profiles[name] < current instructions
Standard Video Template
Use ${SKILL_DIR}/templates/Video.tsx as starting point.
Shared infrastructure — copy only if not already present:
[ ! -f src/remotion/Root.tsx ] && cp ${SKILL_DIR}/templates/Root.tsx src/remotion/
[ ! -d src/remotion/components ] && cp -r ${SKILL_DIR}/templates/components src/remotion/componentsPer-video composition — NEVER overwrite Video.tsx. Create a unique file:
cp ${SKILL_DIR}/templates/Video.tsx src/remotion/{PascalCaseName}Video.tsxRegister in Root.tsx. Each video gets its own composition file.
Localization required.templates/Video.tsxis a zh-CN starter — every visible literal (titles, subtitles, "总结", "感谢观看", outro CTA "点赞 / 收藏 / 关注 / 下期再见!", placeholder bullets) is Chinese. Whenuser_prefs.global.language != "zh-CN", replace every literal in the copied composition file with the target-language equivalent before the Studio preview. Use the platform/language outro table fromworkflow-script.mdStep 4 for the CTA text.
Naming convention:
| Video name | Composition file | Composition ID |
|---|---|---|
ai-agents | AiAgentsVideo.tsx | AiAgents |
reference-manager | ReferenceManagerVideo.tsx | ReferenceManager |
Components are modular:
import { ComparisonCard, CodeBlock, FeatureGrid, MediaSection } from "./components";Component Selection Guide
Choose components based on section content type:
| Content Type | Recommended Component | Draw-On Effect |
|---|---|---|
| Process / pipeline steps | FlowChart | SVG arrow connectors draw progressively |
| History / milestones | Timeline | SVG nodes + connectors animate in sequence |
| Architecture / system diagram | DiagramReveal | Nodes + edges draw on with curve/elbow/straight |
| Comparison / vs | ComparisonCard | Entrance animation |
| Data / metrics | DataBar, StatCounter, MetricsRow | Bar fill + counter animations |
| Code / terminal | CodeBlock | Entrance animation |
| Key quote | QuoteBlock | Entrance animation |
| Feature list / grid | FeatureGrid, IconCard | Staggered entrance |
| Images / screenshots | MediaSection, MediaGrid | Entrance animation |
| After Effects animation | LottieAnimation | Frame-accurate Lottie playback |
Audio visualization — add AudioWaveform as a persistent overlay in the video:
// Inside Video component, after Scale4K but before Audio elements:
<AudioWaveform props={props} position="bottom" mode="bars" barCount={32} height={40} opacity={0.25} />Three modes: "bars" (spectrum), "wave" (filled area), "dots" (pulsing circles).
Diagram architecture — use DiagramReveal for system/architecture diagrams:
<DiagramReveal
props={props}
nodes={[
{ id: "a", label: "Input", x: 100, y: 80 },
{ id: "b", label: "Process", x: 400, y: 80 },
{ id: "c", label: "Output", x: 700, y: 80 },
]}
edges={[
{ from: "a", to: "b", style: "curve" },
{ from: "b", to: "c", style: "curve" },
]}
width={900} height={200}
/>Lottie animations — place JSON files in videos/{name}/animations/:
<LottieAnimation src="animations/brain.json" width={200} height={200} loop />Section Transitions
Template uses @remotion/transitions TransitionSeries.
| Property | Default | Description |
|---|---|---|
transitionType | fade | fade / slide / wipe / none |
transitionDuration | 15 (0.5s) | Frames |
Install dependencies:
npm install @remotion/transitions @remotion/paths @remotion/shapes @remotion/media-utils @remotion/lottie lottie-webKey Architecture
| Point | Description |
|---|---|
| ChapterProgressBar | Must be outside scale(2) container |
| Chapter width | Use flex: ch.duration_frames for proportional width |
| Progress indicator | White progress bar within current chapter |
| 4K scaling | Content area uses scale(2) from 1920×1080 to 3840×2160 |
Triple-Click Outro
Auto mode: Use pre-made MP4 animation. Pick the asset by user_prefs.visual.theme: light → bilibili-triple-white.mp4, dark → bilibili-triple-black.mp4. Interactive mode: Ask: pre-made MP4 (recommended) / Remotion code-generated.
# Light theme:
cp ${SKILL_DIR}/assets/bilibili-triple-white.mp4 videos/{name}/media/
# Dark theme:
cp ${SKILL_DIR}/assets/bilibili-triple-black.mp4 videos/{name}/media/import { OffthreadVideo, staticFile } from "remotion";
<OffthreadVideo src={staticFile("media/bilibili-triple-white.mp4")} />Xiaohongshu: No pre-made animation — use text-based CTA. The outro section renders the CTA text ("点赞收藏加关注,评论区见!") as an animated text overlay, similar to YouTube's text CTA mode.
Douyin: Text-only CTA (no animation). Douyin content is vertical shorts only — the CTA text ("点赞关注,评论区见!") is rendered as simple end text, not animated.
WeChat Channels: Text-only CTA (no animation). WeChat Channels content is vertical shorts only — the CTA text ("点赞关注,转发给朋友!") is rendered as simple end text, not animated.
Preview & Quality Gate (Mandatory Stop)
Remotion Studio is always launched — both auto and interactive modes. This is the primary review step.
Free port 3000 first so a stale Studio from a previous run doesn't serve the wrong --public-dir. Prefer the port-scoped approach below — a naive pkill -f "remotion studio" matches any Remotion Studio across the system, including unrelated projects the user may have open in another terminal:
# Find the PID holding TCP port 3000 (Remotion Studio's default), if any.
STUDIO_PID=$(lsof -nP -iTCP:3000 -sTCP:LISTEN -t 2>/dev/null | head -1)
if [ -n "$STUDIO_PID" ]; then
# Confirm it's actually Remotion before killing — refuses to kill a
# non-Remotion process that happens to bind 3000 (Next.js dev server, etc.)
if ps -p "$STUDIO_PID" -o command= | grep -q "remotion"; then
kill "$STUDIO_PID"
else
echo "Port 3000 is held by a non-Remotion process (PID $STUDIO_PID). Free it manually or run Studio on a different port." >&2
exit 1
fi
fi
npx remotion studio src/remotion/index.ts --public-dir videos/{name}/If you intentionally run multiple Remotion projects in parallel, launch Studio on a non-default port (npx remotion studio ... --port 3001) and adjust the lsof -iTCP:<port> line above accordingly.
1. Launch remotion studio (real-time preview, hot reload) 2. Ask user: "Studio is running at http://localhost:3000. Please review the video preview." 3. Review loop — user reviews, requests changes, the agent applies them, Studio hot reloads:
- Layout/animation tweaks → edit components, Studio auto-refreshes
- Script/content changes → edit
podcast.txt, may need re-TTS (Step 8) - Pronunciation fixes → re-run TTS (Step 8)
4. Exit condition: User explicitly says "render 4K" / "render final version" / "looks good, render" → proceed to Step 10 5. Do NOT proceed to Step 10 until the user confirms.
---
Visual QA (Automated, part of Step 9)
Planned feature. Automated still rendering and multimodal inspection is not yet implemented. Currently, visual quality is verified manually via Remotion Studio preview. The agent may offer to render section stills for manual inspection if requested.
---
Step 10: Render 4K Video
Prerequisite: User has reviewed in Remotion Studio (Step 9) and explicitly requested final render.
4K Render
npx remotion render src/remotion/index.ts CompositionId videos/{name}/output.mp4 --video-bitrate 16M --public-dir videos/{name}/Verify 4K:
ffprobe -v quiet -show_entries stream=width,height -of csv=p=0 videos/{name}/output.mp4
# Expected: 3840,2160Optional: Vertical Highlight Clip (9:16)
npx remotion render src/remotion/index.ts MyVideoVertical videos/{name}/output_vertical.mp4 --video-bitrate 16M --public-dir videos/{name}/
npx remotion still src/remotion/index.ts Thumbnail9x16 videos/{name}/thumbnail_remotion_9x16.png --public-dir videos/{name}/The vertical composition reuses Video.tsx with orientation: "vertical". All components auto-adapt.
Platform-specific video format notes:
- xiaohongshu: Primarily short-form vertical content. Long-form horizontal video is optional.
- douyin: Vertical shorts only (9:16). No horizontal long-form video generated. Uses existing
scripts/generate_shorts.pypipeline. - weixin-channels: Vertical shorts only (9:16). No horizontal long-form video generated. Uses existing
scripts/generate_shorts.pypipeline.
---
Step 11: Mix with Background Music
BGM source single-write rule (READ THIS FIRST). Two paths can layer BGM
on the final video: the Remotion <Audio src="bgm.mp3"> block insideVideo.tsx, and the FFmpegamixbelow. Pick exactly one. Default
behavior is FFmpeg-only —Root.tsx::defaultVideoProps.bgmVolumeis0,
so the Remotion BGM block is disabled and output.mp4 from Step 10contains only narration. Step 11 then layers BGM via FFmpeg.
>
If you intend to bake BGM inside Remotion instead (e.g. for a beat-synced
video where the BGM drives animation): set bgmVolume > 0 in Studio,ensurebgm.mp3is present in--public-dir, and skip Step 11. Running
both layers it twice.
BGM Selection
Default: track is read from user_prefs.bgm.track (logical name) and resolved to a file via user_prefs.bgm.tracks (logical → filename map). Use the helper:
# Copy the configured BGM track to the per-video dir
cp "$(python3 ${SKILL_DIR}/scripts/resolve_bgm_path.py)" videos/{name}/bgm.mp3Override (custom BGM): skip the helper and copy any file:
cp /path/to/user-bgm.mp3 videos/{name}/bgm.mp3Override (different built-in track): edit user_prefs.bgm.track to one of the keys in bgm.tracks (e.g. "calm-piano", "perfect-beauty"). Add new tracks by dropping the mp3 in ${SKILL_DIR}/assets/ and registering it in bgm.tracks.
Mix
BGM volume comes from user_prefs.bgm.volume (fallback 0.10), resolved by scripts/get_pref.py. The narration bus is boosted +3.5 dB (volume=1.5) to lift TTS output (~-26 dB mean) closer to broadcast loudness (~-22 dB mean) without clipping. amix uses normalize=0 so the input-count division (default /N) doesn't halve the narration:
BGM_VOL=$(python3 ${SKILL_DIR}/scripts/get_pref.py global bgm volume --default 0.10)
ffmpeg -y \
-i videos/{name}/output.mp4 \
-stream_loop -1 -i videos/{name}/bgm.mp3 \
-filter_complex "[0:a]volume=1.5[a1];[1:a]volume=${BGM_VOL}[a2];[a1][a2]amix=inputs=2:duration=first:normalize=0[aout]" \
-map 0:v -map "[aout]" \
-c:v copy -c:a aac -b:a 192k \
videos/{name}/video_with_bgm.mp4Why these specific values:
volume=1.5(narration): Azure TTS WAV is typically -25 to -27 dB mean. ×1.5 lifts it to ~-22 dB while keeping ≥2 dB headroom (no clip on common Chinese phonemes).volume=${BGM_VOL}(BGM): default0.10= -20 dB. With narration at 1.5×, this gives ~18 dB BGM-vs-narration headroom — clearly audible but never competing. (Previously0.05was too quiet relative to the boosted narration.)amix=...:normalize=0: prevents amix from dividing each input byinputs=2. Without this, narration gets cut to 50% and the whole video sounds quiet.
Verify loudness after mix:
ffmpeg -i videos/{name}/video_with_bgm.mp4 -af volumedetect -f null - 2>&1 | grep -E "mean_volume|max_volume"
# Target: mean -20 to -22 dB, max -1 to -3 dBIf still too quiet for your platform, add loudnorm=I=-16:TP=-1.5:LRA=11 for EBU R128 broadcast normalization (slower — re-encodes audio).
More BGM options and volume tuning: See references/troubleshooting.md.Video Podcast Maker — Workflow Phase 3: Publish
When to load: Load after the 4K video is rendered with BGM, or when the user asks about subtitles, publish info, cleanup, verification, or generating vertical shorts.
>
Covers: Steps 12-15 (subtitles → publish info → verify + cleanup → optional vertical shorts).
>
Previous phase: See workflow-production.md for Steps 5-11.---
Step 12: Finalize (optional legacy subtitle burn)
Decision point: pick subtitle approach BEFORE Step 10, not here.
>
Subtitles are normally baked into output.mp4 during Step 10's Remotionrender via <Subtitles src={staticFile("podcast_audio.srt")} /> inVideo.tsx. Once Step 10 runs with that component in place, subtitles arealready inside the pixel stream — re-burning at Step 12 would double up.
>
Default (Remotion-native subtitles in Step 10): Step 12 is a finalize/alias
step. Copyvideo_with_bgm.mp4tofinal_video.mp4and move on.
>
Legacy FFmpeg burn (rare): Only choose this if Step 10 was rendered
without the <Subtitles> component (e.g. you removed it for a karaokeeffect, or you're re-burning into an older render). In that case render
Step 10 with subtitles disabled, then run the FFmpeg pass below.
Auto mode: Skip subtitles — copy video_with_bgm.mp4 as final_video.mp4. Interactive mode: Ask user: "Add burned-in subtitles? (Usually not needed — Remotion renders subtitles natively)"
Subtitle Preferences
Read subtitle preferences. If subtitle.enabled == false, skip subtitle burning (copy video_with_bgm.mp4 as final_video.mp4).
If FFmpeg subtitle burn is explicitly requested (legacy/special cases only):
Resolve fontName: "auto" by language:
- zh-CN →
PingFang SC - en-US →
Arial
# Alignment=2: bottom-center. MarginV uses ASS PlayResY (default 288), NOT video pixels.
# MarginV=6 ≈ 6/288 = ~2% from bottom edge, good for all resolutions.
# WARNING: Only burn from video_with_bgm.mp4, NEVER from final_video.mp4 (avoids double-burn).
ffmpeg -y -i videos/{name}/video_with_bgm.mp4 \
-vf "subtitles=videos/{name}/podcast_audio.srt:force_style='FontName=PingFang SC,FontSize=20,PrimaryColour=&H00333333,OutlineColour=&H00FFFFFF,Bold=0,Outline=2,Shadow=0,Alignment=2,MarginV=6'" \
-c:v libx264 -crf 18 -preset slow -s 3840x2160 \
-c:a copy videos/{name}/final_video.mp4If skipping (default for Remotion-native subtitle videos):
cp videos/{name}/video_with_bgm.mp4 videos/{name}/final_video.mp4---
Step 13: Complete Publish Info (Part 2)
Generate Bilibili chapters from timing.json:
00:00 Opening
00:23 Features
00:55 Demo
01:20 SummaryFormat: MM:SS Chapter Title, each gap ≥5s.
Publish Info Format by Platform
Agent behavior: Generate publish info matching platform preference.
bilibili format:
- 标题公式、标签、简介
- 章节时间戳 (if
content.chapters == true)
youtube format:
- SEO-optimized title (<70 chars)
- Keyword-rich description with timestamps
- Tags and hashtags (#tag1 #tag2)
- Chapters (if
content.chapters == true, first line must be0:00)
xiaohongshu format:
- 标题(≤20字)— short, punchy, emoji-friendly
- 正文(200-500字)— 种草/knowledge-sharing style with emoji
- 话题标签 5-10 个,格式
#话题#(双井号) - 无章节时间戳(小红书不支持)
douyin format:
- 文案(100-200字)— casual, emoji-friendly, conversational tone
- 话题标签 3-8 个,格式
#话题(单井号) - 无章节时间戳
- Note: Douyin is shorts-only — no horizontal long-form video
weixin-channels format:
- 文案(100-300字)— knowledge-sharing style, suitable for forwarding
- 话题标签 3-8 个,格式
#话题(单井号) - 无章节时间戳
- Note: WeChat Channels is shorts-only — no horizontal long-form video
---
Step 14: Verify Output & Cleanup
14.1 Verification
Run the unified verifier — it checks all required files, validates technical specs, audits audio/timing alignment, sanity-checks publish_info.md, AND auto-fixes common omissions (e.g. creates final_video.mp4 from video_with_bgm.mp4 when subtitles were skipped but the alias step was missed; disable with --no-fix).
python3 ${SKILL_DIR}/scripts/verify_output.py videos/{name}/Exit codes (preserved across all output formats):
0= all required files present and valid → ready to publish1= critical missing or invalid → fix before publishing2= warnings only → still publishable, review noted issues
Flags:
--strict— treat any warning as a critical issue (exit 1 instead of 2)--no-fix— skip the auto-fix step; preview only (useful for diagnosing what would change before any mutation)--format auto|json|prose—auto(default) emits JSON when stdout is not a TTY; force with--format jsonfor orchestrators that need to parse results programmatically; force prose with--format prose
Structured envelope on --format json:
python3 ${SKILL_DIR}/scripts/verify_output.py videos/{name}/ --format json
# Success: {"ok": true, "data": {"final_video": {...}, "thumbnails": {...}, "audio_timing": {...}, "warnings": [...], "fixes_applied": [...]}, "meta": {...}}
# Failure: {"ok": false, "error": {"code": "validation_failed", "missing_required": [...], "errors": [...], "warnings": [...]}, "meta": {...}}What it checks:
- Required files: podcast.txt, podcast_audio.{wav,srt}, timing.json, output.mp4, final_video.mp4, publish_info.md, both thumbnails
- Final video specs: 3840×2160, h264 + aac, has audio track, duration plausible
- Thumbnail dimensions: 1920×1080 (16:9) and 1200×900 (4:3) — each aspect ratio accepts either
thumbnail_remotion_*.pngorthumbnail_ai_*.png, only flagged missing when both alternatives are absent - Audio/timing drift: WAV duration matches timing.json within 0.5s (uses an audio-only ffprobe pass so .wav containers don't false-fail)
- publish_info.md: contains promo line + per-platform required section headers (bilibili: 标题/标签/简介/章节; youtube: Title/Tags/Description/Chapters; xiaohongshu/douyin/weixin-channels: shorter set without chapters) — resolved from
user_prefs.json→global.platform, defaults to bilibili
What it auto-fixes:
- Creates
final_video.mp4fromvideo_with_bgm.mp4if missing (subtitles-skipped path) - Falls back to
output.mp4if no BGM mix exists yet (with warning to run Step 11)
14.2 Cleanup
Both modes: Only clean TTS temp files (part_.wav, concat_list.txt) automatically. NEVER delete output.mp4 or video_with_bgm.mp4* until the user has reviewed final_video.mp4 and explicitly confirmed it's acceptable. These files are needed to re-do BGM/subtitle steps without a full re-render (~8 min).
VIDEO_DIR="videos/{name}"
# Safe to auto-clean: TTS intermediate files only
rm -f "$VIDEO_DIR"/part_*.wav "$VIDEO_DIR"/concat_list.txt
echo "✓ TTS temp files cleaned"
echo ""
echo "Kept (delete manually after confirming final_video.mp4):"
echo " output.mp4 — clean render without BGM/subtitles"
echo " video_with_bgm.mp4 — render with BGM, no subtitles"14.3 Final Report
=== Video Complete ===
✓ File: final_video.mp4
✓ Resolution: 3840x2160 (4K)
✓ Duration: XXs
✓ Size: XXX MB
✓ Thumbnails: thumbnail_remotion_16x9.png, thumbnail_remotion_4x3.png
✓ Publish info: publish_info.md
✓ Temp files cleaned---
Step 15: Generate Vertical Shorts (Optional)
When: After long-form video is complete (Step 14). Optional step.
Agent behavior: Offer to generate vertical shorts. If user agrees, run automatically.
Generate shorts from sections
python3 ${SKILL_DIR}/scripts/generate_shorts.py --input-dir videos/{name}/ --title "视频标题"This produces videos/{name}/shorts/{section_name}/ for each qualifying section (>20s, not hero/outro) with:
short_audio.wav— extracted audio sliceshort_timing.json— timing for intro (3s) + content + CTA (3s)short_info.json— composition metadataregister_snippet.tsx— Root.tsx registration code
Create short compositions
For each generated short: 1. Copy templates/ShortVideo.tsx as src/remotion/{SectionName}ShortVideo.tsx 2. Replace SectionContent placeholder with the actual section component from the long-form video 3. Update SHORT_CONFIG with values from short_info.json 4. Register composition in Root.tsx using register_snippet.tsx 5. Ensure short_audio.wav is in the short's directory (used via --public-dir)
Render shorts
Each short renders from its own per-short directory because it has its own short_audio.wav and short_timing.json. Match the convention used by generate_shorts.py --render:
npx remotion render src/remotion/index.ts {CompId} \
videos/{name}/shorts/{section}/{CompId}.mp4 \
--video-bitrate 16M \
--public-dir videos/{name}/shorts/{section}/(Do not use --public-dir videos/{name}/ for shorts — Remotion would load the long-form podcast_audio.wav/timing.json instead of the short's sliced assets and the render would drift.)
Each short is a standalone 9:16 4K video (2160×3840) with:
- 3-second intro title card
- Section content (vertical layout, all components auto-adapt)
- 3-second CTA card ("关注看完整版")
Video Podcast Maker — Workflow Phase 1: Scripting
When to load: Load at workflow start, or when the user asks about research, topic definition, or narration script writing.
>
Covers: Pre-workflow (optional design reference extraction) → Startup (load user preferences) → Steps 1-4 (topic → research → sections → script).
>
Next phase: See workflow-production.md for Steps 5-11 (media, TTS, Remotion, render, BGM).---
Pre-workflow: Design Reference (Optional)
When the user provides a reference video/image with their video creation request:
1. Run extraction: python3 scripts/learn_design.py <input> 2. Read extracted frames using your agent's image/file inspection capability 3. Analyze against design-guide.md component vocabulary 4. Present design analysis report to user 5. User confirms/adjusts extracted attributes 6. Apply as session overrides for this video (do NOT save to library unless user asks)
---
Startup: Load User Preferences
Agent behavior: Run the migrator before Step 1. It creates user_prefs.json from the template if absent, deep-merges any new template fields into existing prefs, and applies structural rewrites for old versions. Idempotent (no-op when already current).
SKILL_DIR="${SKILL_DIR:-${CLAUDE_SKILL_DIR}}"
python3 "${SKILL_DIR}/scripts/migrate_prefs.py"Three paths the migrator can take — only the third needs user consent:
1. No prefs file → creates user_prefs.json from the template (safe, no existing data to rewrite). Runs without --yes. 2. Already current version → no-op. Runs without --yes. 3. Existing prefs at older version → exits with confirmation_required (exit 3) because a v1.x → current rewrite would mutate the file in place.
For path 3, do NOT silently retry with `--yes`. Instead:
# Preview the planned changes (always safe, no writes)
python3 "${SKILL_DIR}/scripts/migrate_prefs.py" --dry-run
# Show the user the per-change list, then ask "Apply migration? [y/N]"
# Only after explicit user confirmation:
python3 "${SKILL_DIR}/scripts/migrate_prefs.py" --yesThen read ${SKILL_DIR}/user_prefs.json and apply settings in subsequent steps.
The script prints one of: already at v1.6 — no migration needed, Created user_prefs.json at v1.6 from template, or Migrated from v{old} to v1.6 with a per-change list. To inspect what each version added, see the inline _structural_migrate table in scripts/migrate_prefs.py.
At Step 1 start, inform the user of active preferences (if customized):
"Based on your preferences:
- Platform: [platform] | Language: [language]
- TTS: [tts.backend] / [tts.voices[backend]]
- Speech rate: [tts.rate]
- BGM: [bgm.track] at volume [bgm.volume]
- Subtitles: [enabled/disabled] | CTA: [cta.type]
Say 'set platform youtube' or 'set language en-US' to change.
Say 'show preferences' to see all details."---
Step 1: Define Topic Direction
Auto mode: Infer all decisions from the user's topic description. Use sensible defaults (audience: general, style: educational intro, tone: professional-casual, duration: medium 3-7min). Save directly to videos/{name}/topic_definition.md.
Interactive mode: Confirm each item (use brainstorming skill if available, otherwise ask directly): 1. Target audience: developers / general / students / professionals 2. Video style: educational intro / deep analysis / news brief / hands-on tutorial 3. Content scope: background / technical principles / usage / comparison 4. Tone: serious / casual / fast-paced 5. Duration: short (1-3min) / medium (3-7min) / long (7-15min)
Save to videos/{name}/topic_definition.md
---
Step 2: Research Topic
Use your agent's web search and fetch capabilities. Save to videos/{name}/topic_research.md.
---
Step 3: Design Video Sections
Design 5-7 sections:
- Hero/Intro (15-25s)
- Core concepts (30-45s each)
- Demo/Examples (30-60s)
- Comparison/Analysis (30-45s)
- Summary (20-30s)
Content Density Selection
Assign each section a density tier:
| Tier | Items | Best For |
|---|---|---|
| Impact | 1 | Hook, hero, CTA, brand moment — largest text |
| Standard | 2-3 | Features, comparison, demo |
| Compact | 4-6 | Feature grid, ecosystem |
| Dense | 6+ | Data tables, detailed comparisons — smallest text |
Topic Type Detection
Planned feature. Currently, topic-specific styles are applied manually viauser_prefs.jsonundertopic_patterns. Auto-detection from keywords is not yet implemented.
Title Position
Auto mode: Use top-center (default). Interactive mode: Ask user: top-center (recommended) / top-left / full-center.
Rule: Keep title position consistent within a single video.
---
Step 4: Write Narration Script
Preference application: Adjust script style from user_prefs.content:
tone: professional→ formal languagetone: casual→ conversational, interjections okverbosity: concise→ 50-80 chars per paragraphverbosity: detailed→ 100-150 chars per paragraphheroOpening(if set) → use as fixed hero opening lineoutroClosing(if set) → use as fixed outro closing line
Create videos/{name}/podcast.txt with section markers:
[SECTION:hero]
{heroOpening}(话题引入)...
[SECTION:features]
它有以下功能...
[SECTION:demo]
让我演示一下...
[SECTION:summary]
总结一下,xxx是目前最xxx的xxx。
[SECTION:references]
本期视频参考了官方文档和技术博客。
[SECTION:outro]
{outroClosing}Number formatting for TTS
Write numbers the way you'd naturally type them in a chat message — 2025年, 18个月, 90%, 128GB. Modern TTS (Azure, Edge, Doubao, CosyVoice) reads digit+unit combinations correctly on its own; pre-converting to Chinese characters like 二零二五年 or 十八个月 just makes the script awkward to read and edit. Only spell out in Chinese for the few forms TTS engines genuinely get wrong (see the second table).
✅ Keep as digits (TTS reads naturally — do NOT convert to Chinese):
| Type | Example | Read as |
|---|---|---|
| Year | 2025年, 1998年 | 二零二五年 / 一九九八年 |
| Date | 2025年1月15日, 1月15日 | 二零二五年一月十五日 / 一月十五日 |
| Duration with unit | 18个月, 3年, 45天, 2小时 | 十八个月 / 三年 / 四十五天 / 两小时 |
| Integer with Chinese unit | 2900万, 5亿, 300块 | 二千九百万 / 五亿 / 三百块 |
| Simple percentage | 15%, 90%, -10% | 百分之十五 / 百分之九十 / 负百分之十 |
| Simple decimal | 1.2, 3.5 | 一点二 / 三点五 |
| English unit (tech) | 128GB, 16核, 4K | 一百二十八G / 十六核 / 四K |
| Small integer (<100) | 29, 50 | 二十九 / 五十 |
⚠ Must spell out in Chinese (TTS reads ambiguously or wrong):
| Type | Wrong | Correct |
|---|---|---|
| ISO date with dashes | 2025-01-15 | 2025年1月15日 (or 二零二五年一月十五日) |
| Multi-dot version | v1.2.3 | v一点二点三 |
| Phone / ID string | 400-123-4567 | 四零零 一二三 四五六七 |
| Long bare integer (no unit) | 3999999 | 三百九十九万九千九百九十九 (or rewrite with 万) |
Rule of thumb: prefer digits. Years, dates with 年/月/日, and any number followed by a Chinese unit (年/月/日/个月/天/小时/万/亿/%/GB/块…) should stay as digits — Azure/Edge/Doubao all read them correctly. Only spell out in Chinese when the form is genuinely ambiguous (dash-separated dates, dotted version numbers, phone/ID digit-by-digit, or unitless 7+ digit integers).
Section notes:
- hero: MUST start with
content.heroOpeningif set in user_prefs, followed by the topic hook - summary: Pure content summary, no interaction prompts
- references (optional): One sentence about sources
- outro: MUST use
content.outroClosingif set in user_prefs. Fallback: platform-specific CTA - Empty
[SECTION:xxx]= silent section
Script Template Selection
Copy the script template based on language:
zh-CN→${SKILL_DIR}/templates/podcast_zh.txten-US→${SKILL_DIR}/templates/podcast_en.txt
Outro Text by Platform + Language
| Platform | zh-CN | en-US |
|---|---|---|
| bilibili | "一键三连!评论区留言,下期再见!" | "Like, coin, and favorite! Leave a comment, see you next time!" |
| youtube | "点赞订阅转发!评论区留言,下期再见!" | "Like, subscribe, and share! Leave a comment, see you next time!" |
| xiaohongshu | "点赞收藏加关注,评论区见!" | "Like, save & follow! See you in comments!" |
| douyin | "点赞关注,评论区见!" | "Like & follow! See you in comments!" |
| weixin-channels | "点赞关注,转发给朋友!" | "Like, follow & share with friends!" |
Duration Estimation (Dry Run)
After writing podcast.txt, automatically run:
python3 ${SKILL_DIR}/scripts/generate_tts.py --input videos/{name}/podcast.txt --output-dir videos/{name} --dry-runReport estimated duration. If >12min or <3min, suggest adjustments.
---
Step 4.5: Pronunciation Pre-Flight (zh-CN only)
Skip if `user_prefs.global.language != "zh-CN"`.
Why an LLM step, not code: Polyphone disambiguation needs sentence-level context (一行 → háng for "a line", xíng for "execute"). A regex or static dict can't substitute for reading the script. The phonemes.json system is the output channel; choosing entries is the LLM's job.
Inputs
1. videos/{name}/podcast.txt — the script just written 2. ${SKILL_DIR}/phonemes.json — global dict (already-covered words; do NOT duplicate). Auto-created from ${SKILL_DIR}/phonemes.template.json on the first run of scripts/generate_tts.py, so it always exists by the time TTS executes. To pre-create before the first TTS call: cp "${SKILL_DIR}/phonemes.template.json" "${SKILL_DIR}/phonemes.json". 3. videos/{name}/phonemes.json — project dict (create if missing; takes precedence over global)
Pass 1 — Polyphone scan
Read podcast.txt sentence by sentence. For every Chinese polyphone risk, pick the pronunciation from context.
Reference table: the common-polyphone checklist and pinyin format rules live in [references/zh-polyphones.md](zh-polyphones.md) — load that file now if this is your first pre-flight pass, then return here for Pass 2.
Pass 2 — English term review
mark_english_terms (in scripts/tts/ssml.py) auto-wraps ASCII runs in <lang xml:lang="en-US">, but has known gaps:
- Hyphenated names:
tldraw-cli→ onlyclimay get wrapped;tldrawreads through the voice's default Chinese pronunciation of letters. - Initialisms:
API,URL,MCPare wrapped as words. If you intend letter-by-letter reading, add an inline marker in podcast.txt:配置 API[ei pi ai] 后... - Versioned names:
GPT-4,Claude 4.6— verify the digit reads as digit and the dash reads as space.
For each risky term, prefer editing podcast.txt:
- Inline marker form:
tldraw-cli[tldraw c l i]or rewrite astldraw 命令行工具 - Multi-word phrases already covered by allowlist:
Claude Code,Final Cut Pro,Visual Studio Code,VS Code,Google Chrome,Open AI,OpenAI,GPT 4,GPT-4
Pass 3 — English brand names with Chinese pronunciation
Some products are spelled in English in scripts/code/papers but have an established Chinese pronunciation that listeners expect. The phoneme system handles this cleanly: SSML <phoneme> tag overrides voice pronunciation while leaving the subtitle text unchanged (still shows "Qwen", audio says "千问").
Add to videos/{name}/phonemes.json (or global if it's a stable choice):
{
"Qwen": "qiān wèn",
"Bilibili": "bì lì bì lì"
}Decision rule:
- Has a widely-recognized Chinese name and the script says it in a Chinese-language sentence → add phoneme entry to read it in Chinese.
- Is a code identifier, paper title term, or quoted English brand → leave it as English (don't add).
- Examples to leave alone:
Claude,Gemini,Llama,Mistral,OpenAI,Anthropic,GitHub,Docker,Python— these are read in English in Chinese tech speech.
Use judgment per script. Don't over-translate.
Output
1. Updated videos/{name}/phonemes.json — pretty-printed JSON, longest-key entries first. 2. (Optional) Edits to videos/{name}/podcast.txt for inline English markers or rewrites. 3. Console summary: Pronunciation pre-flight: N polyphone entries added, M English terms flagged. If both 0: No issues found.
Re-run behavior
Always re-scan when this step runs. Existing project-level entries that are still correct should be preserved; new findings get appended. Stale entries (word no longer in podcast.txt) can be left as-is — unused entries cost nothing.
Chinese Polyphone Reference (zh-CN)
When to load: Only during Step 4.5 (Pronunciation Pre-Flight) for zh-CN videos. Reading this whole table for any other purpose wastes context.
This is a non-exhaustive reference for the most common polyphones in tech / explainer videos. Use it as a starting checklist while scanning podcast.txt; rely on linguistic judgment for words not listed here.
Decision rules
- Pick the pronunciation by context, not by single-character lookup. Polyphone disambiguation is what makes this an LLM step instead of a regex.
- Add whole-word entries to
videos/{name}/phonemes.json, never single characters. Whole-word keys avoid catastrophic over-replacement (the applier matches longest-first). - Skip any word already present in the global
${SKILL_DIR}/phonemes.json— duplicate entries waste a phoneme tag and risk drift.
Common polyphones in tech / explainer content
| 字 | Pinyin choices | Typical context |
|---|---|---|
| 行 | háng / xíng | 一行/银行/行业 (háng); 执行/运行/可行/行走 (xíng) |
| 重 | chóng / zhòng | 重做/重新/重复/重试 (chóng); 重要/重量/严重 (zhòng) |
| 长 | cháng / zhǎng | 长度/长期 (cháng); 增长/成长/校长 (zhǎng) |
| 为 | wéi / wèi | 作为/认为/成为 (wéi); 因为/为了 (wèi) |
| 还 | hái / huán | 还有/还是 (hái); 归还/偿还 (huán) |
| 着 | zhe / zháo / zhuó | 跟着/沿着 (zhe); 着火/着急 (zháo); 着手/着重 (zhuó) |
| 差 | chā / chà / chāi | 差别/差距 (chā); 差不多 (chà); 出差 (chāi) |
| 调 | diào / tiáo | 调研/调查/语调 (diào); 调整/协调 (tiáo) |
| 分 | fēn / fèn | 分钟/分开/分类 (fēn); 缘分/部分/养分 (fèn) |
| 中 | zhōng / zhòng | 中间/中央 (zhōng); 中奖/命中 (zhòng) |
| 处 | chǔ / chù | 处理/相处 (chǔ); 到处/好处 (chù) |
| 间 | jiān / jiàn | 中间/时间/期间 (jiān); 间隔/间断 (jiàn) |
| 给 | gěi / jǐ | 给我/给你 (gěi); 供给/补给 (jǐ) |
| 教 | jiāo / jiào | 教书/教课 (jiāo); 教育/宗教 (jiào) |
| 模 | mó / mú | 模型/模式/规模 (mó); 模样/一模一样 (mú) |
| 量 | liàng / liáng | 数量/质量/分量 (liàng); 测量/丈量 (liáng) |
| 觉 | jué / jiào | 感觉/觉得 (jué); 睡觉/午觉 (jiào) |
| 应 | yīng / yìng | 应该/应当 (yīng); 答应/反应/适应 (yìng) |
| 干 | gān / gàn | 干净/相干 (gān); 干活/能干 (gàn) |
| 转 | zhuǎn / zhuàn | 转弯/转换 (zhuǎn); 转动/旋转 (zhuàn) |
| 划 | huá / huà | 划船/划算 (huá); 计划/规划 (huà) |
| 数 | shù / shǔ | 数字/次数 (shù); 数数/数一数 (shǔ) |
| 当 | dāng / dàng | 当然/应当 (dāng); 适当/上当 (dàng) |
| 占 | zhān / zhàn | 占卜 (zhān); 占据/占领 (zhàn) |
| 假 | jiǎ / jià | 假如/假设/假装 (jiǎ); 放假/假期 (jià) |
| 倒 | dǎo / dào | 倒下/倒闭 (dǎo); 倒车/倒影/倒是 (dào) |
| 几 | jī / jǐ | 几乎/茶几 (jī); 几个/几次 (jǐ) |
| 卡 | kǎ / qiǎ | 卡片/打卡 (kǎ); 卡住/关卡 (qiǎ) |
Pinyin format requirements
- Use tone marks (ā á ǎ à), space-separated syllables:
"一行命令": "yì háng mìng lìng". - Never use bare single-character keys like
"行": "háng". - The TTS layer converts tone-marked pinyin to SAPI numeric tones automatically; you don't need to do that conversion yourself.
"""TTS package for Video Podcast Maker."""
Related skills
FAQ
What does video-podcast-maker output?
A 4K MP4 video with narration, background music, thumbnails, and a publish_info.md, produced from a research-to-Remotion pipeline.
Which TTS backends are supported?
Edge (free default), Azure, Doubao, CosyVoice, ElevenLabs, OpenAI, and Google TTS.