Imported from Migeking/video-production-skills (
xhs-video-pipeline/SKILL.md). Install upstream withnpx skills add Migeking/video-production-skills --skill xhs-video-pipeline. Copyright stays with the author.
小红书视频制作流水线(不含自动发布)
你是"小红书视频制作助手"。目标是将 小红书笔记/ 目录下的 Markdown 文章转换为视频。
⚠️ 禁止任何形式的自动浏览器发布操作。
调用入口
本 SKILL 由 xhs-video-maker 编排器根据用户偏好路由后调用。
用户偏好确认(翻译 / TTS / BGM 风格)已由编排器完成,此处直接开始实现。
视频规范: 1080×1440(3:4 竖版高清),GSAP 截图 + BGM + 可选 TTS 配音。
核心文件路径(固定)
源文章: D:\code\MyWord\小红书笔记\[文章名].md
动画 HTML: D:\code\MyWord\xhs-output\[文章名]_animated.html
截图帧目录: D:\code\MyWord\xhs-output\slides\frame_%04d.png
输出视频: D:\code\MyWord\xhs-output\[文章名]_tts_bgm.mp4
截图脚本: D:\code\MyWord\scripts\capture_xhs_frames.py
TTS 目录: D:\code\MyWord\xhs-output\[文章名]\assets\
BGM 目录: D:\code\MyWord\scripts\assets\bgm\
Step 0:环境检查(每次执行前必做)
# 验证 FFmpeg 已安装
ffmpeg -version
预期输出: FFmpeg 正常。
Pipeline A:GSAP 截图 + BGM(默认)
Step 1:创建动画幻灯片 HTML
1.1 理解源内容
读取 小红书笔记/ 目录下的源 Markdown 文件,分析其结构:
- 文章标题(hero 区域)
- 章节数(约 6-10 个主要 section)
- 每个章节的核心观点、金句、数据
内容精简原则(⛔ 重要):
- 每张 slide 只说一个核心点,不要列举
- 去掉 feature grid、bullet list、tag 集群 — 这些在视频里看不清
- 保留标题 + 一句核心文案即可,最多两句
- 科技/技术类文章可加一行代码片段或标签,但不要超过三行
1.2 创建 1080×1440 动画 HTML
分辨率要求:至少 1080×1440 px(3:4 竖版)。旧版 390×844 已废弃。
输出路径: D:\code\MyWord\xhs-output\[文章名]_animated.html
技术规范:
| 参数 | 值 |
|---|---|
| 画布尺寸 | 1080×1440 px(3:4 竖版) |
| 动画引擎 | GSAP 3.x(CDN: cdnjs.cloudflare.com) |
| 字体 | PingFang SC / Microsoft YaHei |
| 配色 | 深色背景(#0a0e17)+ 品牌橙色(#ff6b35)/ 可根据品牌自定义 |
| Slide 数量 | 6-10 页(不要超过 10 页) |
| 单页时长 | 3.5-4.5s |
| 总时长 | ~25-36s |
| 总帧数 | 250-360 帧(@10fps) |
| 视频码率 | CRF 23(平衡质量/体积) |
关键设计原则(基于实战验证):
| # | 原则 | 说明 |
|---|---|---|
| 1 | 字体要大 | 标题 72-96px,section-title 56-82px,正文 34-48px |
| 2 | 每页只说一句话 | 去掉 feature grid / bullet / tags,只留核心主题 |
| 3 | 深色背景 + 高对比 | bg #0a0e17,文字白色,强调色橙色/红色 |
| 4 | 每个 slide 0.5s 淡入 | opacity + translateY 动画 |
| 5 | 背景网格 + 光晕装饰 | 增加视觉层次感,不单调 |
| 6 | 10 页以内 | 超过 10 页用户会失去耐心,3.5-4.5s/页 |
推荐字体大小对照表:
| 用途 | 字号 | CSS 类名 |
|---|---|---|
| 特大标题(首页) | 82-96px | .title-main |
| 章节标题 | 72-82px | .section-title |
| 大号正文 | 42-48px | .body-big |
| 中号正文 | 34-40px | .body-mid |
| 副标题/标签 | 28-32px | .sub-title |
| 水印 | 14px | .watermark |
HTML 模板(1080×1440,大字版):
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1080, initial-scale=1.0">
<title>文章标题</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:"PingFang SC","Microsoft YaHei",sans-serif;
background:#0a0e17;color:#fff;overflow:hidden;
width:1080px;height:1440px;position:relative}
:root{--accent:#ff6b35;--accent-light:#ff8a5c}
.bg-grid{position:absolute;top:0;left:0;width:100%;height:100%;
background-image:
linear-gradient(rgba(255,107,53,0.03) 1px,transparent 1px),
linear-gradient(90deg,rgba(255,107,53,0.03) 1px,transparent 1px);
background-size:80px 80px;pointer-events:none}
.glow-dot{position:absolute;width:300px;height:300px;border-radius:50%;
filter:blur(80px);pointer-events:none;opacity:0.3}
.status-bar{position:absolute;top:0;left:0;right:0;height:3px;
background:linear-gradient(90deg,var(--accent),var(--accent-light),var(--accent));
background-size:200% 100%;z-index:100}
.slide{position:absolute;top:0;left:0;width:1080px;height:1440px;
padding:80px 80px;display:flex;flex-direction:column;
opacity:0;pointer-events:none}
.scroll-content{flex:1;display:flex;flex-direction:column;justify-content:center}
.title-main{font-size:82px;font-weight:800;color:#fff;line-height:1.2}
.title-main .ac{color:var(--accent)}
.section-title{font-size:72px;font-weight:800;color:#fff;line-height:1.2}
.section-title .ac{color:var(--accent)}
.body-big{font-size:42px;color:rgba(255,255,255,0.75);line-height:1.6;font-weight:500}
.body-mid{font-size:34px;color:rgba(255,255,255,0.6);line-height:1.6}
.sub-title{font-size:32px;color:rgba(var(--accent),0.7);font-weight:600;letter-spacing:2px}
.divider{width:60px;height:3px;background:linear-gradient(90deg,var(--accent),transparent);margin:16px 0 24px}
.divider-center{width:60px;height:3px;
background:linear-gradient(90deg,transparent,var(--accent),transparent);
margin:16px auto 24px}
.tagline{font-size:26px;color:rgba(255,255,255,0.3);text-align:center;margin-top:40px}
.watermark{position:absolute;bottom:40px;right:70px;
font-size:14px;color:rgba(255,255,255,0.12);letter-spacing:4px;font-weight:300}
.badge{display:inline-block;background:rgba(255,107,53,0.08);border-radius:30px;
padding:8px 24px;border:1px solid rgba(255,107,53,0.15)}
.badge span{font-size:26px;color:rgba(255,255,255,0.6)}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
</head>
<body>
<div class="status-bar"></div>
<div class="bg-grid"></div>
<!-- 每个 slide 一个 div -->
<div class="slide" id="s1">...</div>
<div class="slide" id="s2">...</div>
<!-- ... 6-10 页 -->
<script>
const tl = gsap.timeline({repeat:-1, paused:false});
// 每页停留秒数,和 slide 数量对应
const durations = [4.0, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5];
for(let i=1; i<=durations.length; i++){
const prev = i===1 ? durations.length : i-1;
const stay = durations[i-1];
tl.to('#s'+prev, {opacity:0, pointerEvents:'none', duration:0.3}, '+=0')
.fromTo('#s'+i, {opacity:0,y:30},
{opacity:1,y:0,pointerEvents:'auto',duration:0.5,ease:'power2.out'})
.to({}, {duration: stay - 0.8});
}
</script>
</body>
</html>
Step 2:Playwright 截图
2.1 修改截图脚本
每次使用时修改 capture_xhs_frames.py 中的 3 处:
html_path:指向 Step 1 创建的 HTML 文件viewport:设置为{"width": 1080, "height": 1440}total_frames:= 视频总时长(s)× 10fps
"""Capture frames at 10fps from animated HTML."""
import asyncio, os, glob
from playwright.async_api import async_playwright
async def capture():
html_path = os.path.abspath("D:/code/MyWord/xhs-output/[文章名]_animated.html")
slides_dir = os.path.abspath("D:/code/MyWord/xhs-output/slides")
os.makedirs(slides_dir, exist_ok=True)
for f in glob.glob(os.path.join(slides_dir, "*.png")): os.remove(f)
async with async_playwright() as p:
browser = await p.chromium.launch(
headless=True,
args=["--no-sandbox","--disable-setuid-sandbox","--disable-web-security"])
page = await browser.new_page(viewport={"width":1080,"height":1440})
await page.goto(f"file:///{html_path.replace(chr(92),'/')}")
await page.wait_for_timeout(4000)
total_frames = 350 # 根据动画总时长调整
for i in range(total_frames):
await page.screenshot(
path=os.path.join(slides_dir, f"frame_{i:04d}.png"))
if i < total_frames - 1: await asyncio.sleep(0.1)
await browser.close()
print(f"Done: {len(glob.glob(slides_dir+'/frame_*.png'))} frames")
asyncio.run(capture())
2.2 执行
cd D:\code\MyWord
python scripts\capture_xhs_frames.py
预期: N 张 1080×1440 PNG 截图保存在 xhs-output/slides/
Step 3:BGM 选曲与下载
3.1 选曲原则
| 文章主题 | 推荐曲目 | 风格 |
|---|---|---|
| 工业/科技/管理 | Concentration, New Direction, Sovereign, Elevate | 现代、专业、科技电子感 |
| 商业/财经/思维 | Clean Soul, Ebbs and Flows, Almost in F | 干练、流畅 |
| 励志/成长/故事 | Touching Story, Soaring, Infinite Perspective | 温暖、向上 |
| 知识/教育/科普 | Fluidscape, Ambiment, Perspectives | 中性、专注 |
3.2 列出 BGM
Get-ChildItem "D:\code\MyWord\scripts\assets\bgm"
BGM 保存到 D:\code\MyWord\scripts\assets\bgm\。共 42 首 CC BY 4.0 曲目。
3.3 署名要求
Music: Kevin MacLeod (incompetech.com) - Licensed under CC BY 4.0
Step 3.5:TTS 配音生成(可选)
3.5.1 编写配音文案
根据 slide 内容,为每张 slide 写一句自然旁白。
保存到 D:\code\MyWord\xhs-output\[文章名]\script.txt。
文案原则:
- 每 slide 一句,6-10 句
- 每句 3-5 秒(15-25 字)
- 数字用中文全称:
80%→百分之八十,2024年→二零二四年 - 避免特殊字符:
.NET写成点NET,v1.0写成一点零版或直接正式版 - 保留核心数据、金句,去掉冗余修饰
⚠️ 已知坑:ChatTTS 中文数字发音问题
ChatTTS 遇到 80%、20%、v1.0 等会读英文。一律用中文全称:
| 错误写法 | 正确写法 | ChatTTS 发音 |
|---|---|---|
80% |
百分之八十 |
✅ 中文 |
2024年 |
二零二四年 |
✅ 中文 |
v1.0 |
正式版 或 一点零版 |
✅ 中文 |
.NET |
点NET |
✅ 中文 |
3.5.2 生成 TTS 音频(ChatTTS 推荐)
✅ 推荐方案:单段连续文本(人声一致)
ChatTTS 逐句生成时,不同句子的音色有微妙差异。单段连续文本一次 infer() 保证 100% 同一人声。
"""Generate single continuous TTS voiceover (recommended for consistent voice)."""
import os, ChatTTS, soundfile as sf
os.chdir(os.path.dirname(os.path.abspath(__file__)))
# 把所有旁白连成一段,句号分隔
full_text = "第一句旁白。第二句旁白。第三句旁白。"
os.makedirs('assets', exist_ok=True)
chat = ChatTTS.Chat()
chat.load(source='huggingface') # ⚠️ 新版 API: load() 而不是 load_models()
wav = chat.infer(
[full_text],
skip_refine_text=True,
params_infer_code=ChatTTS.Chat.InferCodeParams( # ⚠️ 必须用 InferCodeParams 对象
prompt='[speed_5]',
manual_seed=42 # ⚠️ manual_seed 而不是 seed
)
)
audio_data = wav[0][0] if wav[0].ndim > 1 else wav[0]
sf.write('assets/voiceover.wav', audio_data, 24000)
print(f'Done! Duration: {len(audio_data)/24000:.2f}s')
备选方案:逐句生成(不推荐,音色可能不一致)
# 旧方案 — 音色可能有微妙差异
for i, text in enumerate(texts, 1):
wav = chat.infer(text, skip_refine=True, params_infer_code={
'prompt': '[speed_5]', 'seed': 42 # ❌ dict+seed 方式在某些版本报错
})
备选引擎:edge-tts(在线)
edge-tts --file "xhs-output\[文章名]\script.txt" `
--voice zh-CN-YunxiNeural `
--rate +40% `
--write-media "xhs-output\[文章名]\voiceover.mp3"
输出文件统一路径:
- WAV:
xhs-output\[文章名]\assets\voiceover.wav - MP3:
xhs-output\[文章名]\voiceover.mp3(edge-tts)
Step 4:FFmpeg 合成视频(BGM + TTS)
模式 A:纯 BGM(无 TTS)
cd D:\code\MyWord
ffmpeg -y -framerate 10 -i "xhs-output\slides\frame_%04d.png" `
-i "scripts\assets\bgm\[曲目名].mp3" `
-c:v libx264 -pix_fmt yuv420p -preset medium -crf 23 `
-c:a aac -b:a 128k -shortest `
"xhs-output\[文章名]_1080p.mp4"
模式 B:TTS + BGM 混音(推荐)
cd D:\code\MyWord
ffmpeg -y -framerate 10 -i "xhs-output\slides\frame_%04d.png" `
-i "xhs-output\[文章名]\assets\voiceover.wav" `
-i "scripts\assets\bgm\[曲目名].mp3" `
-c:v libx264 -pix_fmt yuv420p -preset medium -crf 23 `
-filter_complex `
"[1:a]adelay=500|500[tts];`
[2:a]volume=0.12[bgm];`
[tts][bgm]amix=inputs=2:duration=longest[a]" `
-map 0:v -map "[a]" -c:a aac -b:a 192k -shortest `
"xhs-output\[文章名]_tts_bgm.mp4"
参数说明:
| 参数 | 值 | 说明 |
|---|---|---|
adelay=500|500 |
TTS 延迟 500ms | 留出封面淡入时间 |
volume=0.12 |
BGM 降音 | TTS 存在时 12-15% |
amix:duration=longest |
以最长为总长 | 避免被截短 |
-shortest |
截断 | 确保视频不超音频 |
⚠️ 中文文件名编码问题:
Windows cmd/PowerShell 下中文文件名显示为乱码(如 A2A-�����ư�������ָ��_tts_bgm.mp4),但文件实际存在。
建议合成后同时复制一份 ASCII 英文别名:
Copy-Item -LiteralPath "xhs-output\[文章名]_tts_bgm.mp4"`
-Destination "xhs-output\[英文短名].mp4"
验证输出:
ffprobe -v error -show_entries format=size,duration:stream=width,height,codec_name `
-of default=noprint_wrappers=1 "xhs-output\[文章名]_tts_bgm.mp4"
预期: H.264 1080×1440 + AAC,~25-36s,~1-3MB。
Pipeline B(HyperFrames + TTS)由编排器
xhs-video-maker路由到xhs-video-workflowSKILL 执行。
常见问题(实战踩坑汇总)
Pipeline A
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 分辨率不够高清 | 用了 390×844 旧规范 | 必须用 1080×1440 |
| 字体太小看不清 | 默认模板字体偏小 | 标题 72-96px,正文 34-48px(见上表) |
| 内容太密读不完 | 放了 feature grid / bullet list | 每页只说一个点,精简 |
| TTS 音色不一致 | 逐句生成,每组音色不同 | 改为单段连续文本一次 infer |
chat.load_models 报错 |
ChatTTS 新版 API 变更 | 改为 chat.load(source='huggingface') |
seed 参数报错 |
新版用 manual_seed 且需 InferCodeParams 对象 |
见上方示例 |
中文文件名终端显示 ??? |
Windows 控制台编码问题 | 用英文别名复制一份 |
| BGM 下载太慢 | Incompetech 服务器在国外 | 用 curl -L -r 0-5000000 |
| UnicodeEncodeError | Windows 编码问题 | 路径用英文/ASCII,写文件用 UTF-8 |
| ChatTTS 数字读英文 | 80% → "eighty percent" |
文案中用 百分之八十 |
Pipeline B
| 问题 | 解决方案 |
|---|---|
| slide 空白 | 加 .slide.clip{opacity:1!important} CSS |
| GSAP 动画错乱 | 确保 GSAP <script> 只出现一次 |
| 首帧封面空白 | 封面标题不用 .word 类 |
| 配音画面不同步 | data-start = slide start + 0.3s |
| BGM 盖过配音 | data-volume="0.15" |
执行时间参考
Pipeline A
| 步骤 | 耗时 |
|---|---|
| Step 1 创建动画 HTML(6-10 页大字版) | ~5-8 分钟 |
| Step 2 截图(250-360 帧 @10fps) | ~40-60 秒 |
| Step 3.5 ChatTTS 生成配音 | ~2-5 分钟 |
| Step 4 FFmpeg 合成 | ~10 秒 |
| 总计 | ~10-15 分钟 |
Pipeline B
| 步骤 | 耗时 |
|---|---|
| 设计幻灯片 HTML | ~10-15 分钟 |
| ChatTTS 配音 | ~3-5 分钟 |
| timing.json | ~10 秒 |
| 集成音频 | ~5 分钟 |
| 渲染 MP4 | ~5-15 分钟 |
| 总计 | ~25-40 分钟 |
核心文件索引
| 资源 | 路径 |
|---|---|
| 源文章 | D:\code\MyWord\小红书笔记\[文章名].md |
| 动画 HTML | D:\code\MyWord\xhs-output\[文章名]_animated.html |
| 截图帧 | D:\code\MyWord\xhs-output\slides\frame_%04d.png |
| 截图脚本 | D:\code\MyWord\scripts\capture_xhs_frames.py |
| TTS 配音 | D:\code\MyWord\xhs-output\[文章名]\assets\voiceover.wav |
| BGM 库 | D:\code\MyWord\scripts\assets\bgm\(42 首) |
| 输出视频 | D:\code\MyWord\xhs-output\[文章名]_tts_bgm.mp4 |
版本历史
| 版本 | 日期 | 变更 |
|---|---|---|
| v1.1 | 2026-05-29 | ChatTTS API 修复(load→load_models, InferCodeParams, manual_seed);字体全面加大(82px+);内容精简原则;单段连续文本 TTS 方案;中文编码问题处理 |
| v1.0 | - | 初始版本 |