1. 从一份 Word 稿到 142 秒竖屏视频,这套流程到底解决了什么问题
手里有一份写好的 Word 口播稿,想把它变成一条能直接发出去的竖屏短视频,这件事听起来简单,做起来全是坑。我最初的想法也很朴素:把稿子念一遍录下来,找个剪辑软件配上字幕和背景,导出完事。结果第一次尝试就卡了整整一个下午——录音环境有底噪,字幕对轴对到眼花,竖屏构图要重新裁切,背景音乐音量忽大忽小,最后导出发现分辨率不对,平台压缩之后糊成一团。
后来我把这件事拆开看,发现它本质上是一条内容转换流水线:输入是纯文本,输出是带字幕、带配音、带背景、带封面的竖屏 MP4。中间要经过文本清洗、语音合成、时间轴对齐、画面渲染、音视频合成五个环节。每个环节单独看都不复杂,但串起来之后,任何一个环节的参数不对,最终成品就会出问题。
这套流程跑通之后,我实测了一份 380 字左右的 Word 稿,最终产出142 秒的竖屏口播视频,从文本到成片全程自动化,重复生产只需要替换文稿内容。这篇文章就把整条链路拆开讲清楚,包括我为什么选这些工具、每个环节的关键参数怎么定、踩过哪些坑、以及怎么让这套流程稳定复现。
适合谁来参考?如果你是需要批量产出短视频口播内容的自媒体从业者、做知识付费需要把课程稿转成视频的老师、或者单纯想把手里的文字素材快速视频化的开发者,这套方案都能直接抄作业。不需要你有专业的剪辑经验,但需要你能跑命令行、能看懂基础的 HTML 和 JavaScript。
核心关键词先摆出来:HTML 负责画面结构,edge-tts 负责语音合成,Remotion 负责把画面渲染成视频帧,FFmpeg 负责最终的音视频合成与格式转换,Playwright 负责在需要时做页面截图或自动化校验。这五个工具各司其职,组合起来就是一条完整的文本到视频的生产线。
2. 整体方案设计与工具选型逻辑
2.1 为什么不用剪映或 PR 这类现成工具
很多人第一反应是:为什么不直接用剪映?剪映确实能做出很好的口播视频,但它的问题在于不可编程。你没法用脚本批量替换文稿、批量生成配音、批量对齐字幕。当你只有一条视频的时候,手动剪没问题;当你有二十条、五十条的时候,手动剪就是灾难。
我需要的是一个可复现、可批量、可版本控制的流程。文稿是 Markdown 或 Word,配音是脚本生成的音频文件,画面是代码写的 HTML 模板,最终合成是命令行完成的。这样每次生产新视频,我只需要换文稿,其他全部自动跑完。
另一个考虑是成本。商用语音合成按字数收费,一条两分钟的视频可能就要几块钱,批量生产下来成本不低。而 edge-tts 是本地可调用的语音合成方案,音质在口播场景下完全够用,中文自然度也过得去,关键是零成本、可离线、可批量。
2.2 五个工具的分工与协作关系
这套流程里每个工具的角色非常清晰,我用一张表来说明:
| 工具 | 负责环节 | 输入 | 输出 |
|---|---|---|---|
| edge-tts | 语音合成 | 纯文本 | MP3 音频 + 时间轴信息 |
| HTML + CSS | 画面模板 | 文稿内容 | 可视化的竖屏页面 |
| Remotion | 视频渲染 | HTML 组件 + 音频 | 逐帧视频文件 |
| FFmpeg | 音视频合成 | 视频帧 + 音频 | 最终 MP4 |
| Playwright | 页面校验/截图 | HTML 页面 | 截图或 DOM 状态 |
这里要特别说明一下Remotion 的定位。Remotion 是一个用 React 写视频的工具,它把视频的每一帧都当成一个 React 组件来渲染。这意味着你可以用写网页的方式写视频——用 CSS 控制样式,用 JavaScript 控制动画,用 props 传数据。对于我这种前端背景的人来说,这比学 AE 的表达式要顺手得多。
而Playwright在这套流程里不是必须的,但在两个场景下很有用:一是当你的画面模板比较复杂,需要先截图确认渲染效果时;二是当你想从某个网页抓取内容作为视频素材时。它的作用是把浏览器变成可控的渲染环境,让你能在无头模式下拿到页面的最终视觉状态。
2.3 为什么最终选择 FFmpeg 做合成
Remotion 本身可以导出视频,但它导出的是逐帧渲染后的结果,音频和视频的合并、编码格式的转换、码率的控制,还是交给 FFmpeg 更稳妥。FFmpeg 是音视频处理领域的瑞士军刀,参数多但可控性强。我最终输出的竖屏视频规格是1080x1920,30fps,H.264 编码,AAC 音频,码率控制在 6Mbps 左右,这个规格在主流平台上传后压缩损失最小。
选 FFmpeg 还有一个原因是它能把字幕烧进视频。虽然平台支持外挂字幕,但竖屏视频里字幕是视觉的一部分,烧进去更可控。FFmpeg 的 subtitles 滤镜可以直接把 SRT 或 ASS 字幕文件叠加到视频上,字体、大小、位置、描边都能调。
3. 核心细节解析与实操要点
3.1 文稿预处理:从 Word 到可合成的文本
Word 稿不能直接丢给语音合成,里面有一堆需要清理的东西:多余的空格、全角半角混用、换行符不统一、中英文标点混杂。我一般会先把 Word 内容复制到纯文本编辑器里,做三件事:
第一,统一标点。中文口播稿里的逗号、句号、问号、感叹号全部用全角,英文和数字保留半角。标点直接影响语音合成的停顿节奏,全角逗号的停顿时长比半角长,听起来更自然。
第二,处理数字和英文。edge-tts 对纯数字的读法有时候会出错,比如"2024"可能读成"两千零二十四"而不是"二零二四"。我的做法是在文稿阶段就把需要逐字读的数字写成中文,比如"二零二四",或者在 edge-tts 的参数里指定读法。
第三,分段。口播稿按语义分成短段,每段不超过 80 个字。太长的段落合成出来的音频节奏会拖沓,而且后期对齐字幕时不好处理。分段之后,每段单独合成音频,再按顺序拼接,这样时间轴更清晰。
提示:Word 里的智能引号和弯引号在复制到纯文本后经常会变成乱码,建议先在 Word 里用查找替换把弯引号换成直引号,再复制出来。
3.2 edge-tts 的参数调优:语速、音色与停顿
edge-tts 的命令行调用很简单,但参数调不好,出来的音频会很机械。我常用的命令是这样的:
edge-tts --voice zh-CN-YunxiNeural --rate=+8% --pitch=-2Hz --text "你的口播文稿内容" --write-media output.mp3 --write-subtitles output.srt几个关键参数的解释:
- --voice:中文男声我常用
zh-CN-YunxiNeural,女声用zh-CN-XiaoxiaoNeural。云希的音色偏年轻,适合知识类口播;晓晓的音色更柔和,适合情感类内容。 - --rate=+8%:语速加快 8%。默认语速偏慢,口播视频需要稍微快一点才有节奏感。但不要超过 +15%,否则听起来会赶。
- --pitch=-2Hz:音调降低 2Hz。稍微降低音调会让声音更沉稳,适合知识分享类内容。如果是轻松的内容,可以不加这个参数。
- --write-subtitles:这个参数会同时生成 SRT 字幕文件,时间轴和音频完全对齐,省去了手动对轴的麻烦。
实测下来,380 字的文稿用 +8% 语速合成,音频时长大约在 140 秒左右,和最终视频的 142 秒基本吻合,中间多出来的 2 秒是片头片尾的留白。
3.3 HTML 画面模板的设计要点
竖屏视频的画面模板用 HTML 写,核心是安全区的概念。1080x1920 的画布上,上下各留 250px 左右的安全边距,因为很多平台会在顶部和底部叠加 UI 元素。真正的内容区域大概在 1080x1420 左右。
我的模板结构大概是这样的:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <style> body { margin: 0; width: 1080px; height: 1920px; background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%); font-family: "Noto Sans SC", sans-serif; display: flex; flex-direction: column; justify-content: center; align-items: center; padding: 250px 80px; box-sizing: border-box; } .title { font-size: 72px; color: #ffffff; font-weight: 700; line-height: 1.3; text-align: center; margin-bottom: 60px; } .subtitle { font-size: 48px; color: #e0e0e0; line-height: 1.5; text-align: center; } </style> </head> <body> <div class="title">主标题内容</div> <div class="subtitle">副标题或正文内容</div> </body> </html>这个模板的关键点在于:字体大小要足够大。竖屏视频在手机上看,标题字号至少 72px,正文字号至少 48px,否则在手机上看起来会很小。行高控制在 1.3 到 1.5 之间,太密了看不清,太疏了显得空。
背景我用的是深色渐变,因为深色背景在手机上更省电,而且白色文字对比度高,看起来更清晰。如果你做的是轻松内容,可以用浅色背景配深色文字,但要注意对比度。
3.4 Remotion 的渲染配置与帧率控制
Remotion 的项目初始化用npx create-video@latest,选空白模板就行。核心的渲染逻辑写在src/Root.tsx和src/Composition.tsx里。
Composition 的配置决定了视频的基本规格:
export const MyComposition = () => { return ( <Composition id="OralVideo" component={OralVideo} durationInFrames={142 * 30} fps={30} width={1080} height={1920} defaultProps={{ title: "默认标题", subtitle: "默认副标题", }} /> ); };durationInFrames是总帧数,等于视频秒数乘以帧率。142 秒的视频,30fps,就是 4260 帧。这个数字要和音频时长对齐,否则会出现音画不同步。
Remotion 渲染命令:
npx remotion render OralVideo out/video.mp4 --props='{"title":"实际标题","subtitle":"实际副标题"}'渲染速度取决于机器性能,1080x1920 的分辨率下,4260 帧大概需要 3 到 5 分钟。如果觉得慢,可以先把--concurrency调高,利用多核 CPU 并行渲染。
注意:Remotion 渲染出来的视频默认没有音频,音频需要在 FFmpeg 合成阶段加进去。
3.5 FFmpeg 合成:把画面、音频、字幕拼在一起
FFmpeg 的合成命令是整个流程里参数最多的一步,我把它拆成三个子步骤来做,这样出错了容易定位。
第一步,把 Remotion 输出的无声视频和 edge-tts 生成的音频合并:
ffmpeg -i video_silent.mp4 -i audio.mp3 -c:v copy -c:a aac -b:a 192k -shortest output_with_audio.mp4-c:v copy表示视频流不重新编码,直接复制,这样速度快、画质无损。-shortest表示以较短的流为准,防止音频比视频长导致最后几秒黑屏。
第二步,把 SRT 字幕烧进视频:
ffmpeg -i output_with_audio.mp4 -vf "subtitles=output.srt:force_style='FontName=Noto Sans SC,FontSize=18,PrimaryColour=&HFFFFFF,OutlineColour=&H000000,Outline=2,Alignment=2,MarginV=120'" -c:a copy output_final.mp4字幕样式参数说明:
- FontSize=18:字号,这个数值是相对于视频高度的比例,18 在 1080x1920 下大概对应 48px 的实际字号。
- PrimaryColour=&HFFFFFF:白色字体。
- OutlineColour=&H000000:黑色描边。
- Outline=2:描边宽度 2px。
- Alignment=2:底部居中。
- MarginV=120:距离底部 120px,避开平台底部的 UI 区域。
第三步,如果还需要压缩码率或转换格式,可以再加一道转码:
ffmpeg -i output_final.mp4 -c:v libx264 -preset medium -crf 23 -c:a aac -b:a 128k -movflags +faststart final.mp4-crf 23是画质和文件大小的平衡点,数值越小画质越好但文件越大。-movflags +faststart把元数据移到文件头部,方便在线播放时快速加载。
4. 完整实操流程与关键环节实现
4.1 环境准备与依赖安装
这套流程涉及的工具比较多,我建议按顺序安装,避免依赖冲突。
Node.js 环境:Remotion 和 Playwright 都依赖 Node.js,建议用 18.x 或 20.x 的 LTS 版本。安装完之后用node -v确认版本。
edge-tts:这是一个 Python 包,用 pip 安装:
pip install edge-tts安装完之后用edge-tts --list-voices可以列出所有可用音色,确认中文音色存在。
FFmpeg:Windows 用户去官网下载编译好的二进制文件,解压后把bin目录加到系统 PATH 里。macOS 用户用brew install ffmpeg。安装完之后用ffmpeg -version确认。
Remotion:在项目目录下用npx create-video@latest初始化,按提示选择模板。初始化完成后npm install安装依赖。
Playwright:如果只是做页面校验,用npm install playwright然后npx playwright install chromium安装浏览器内核。如果安装失败,通常是网络问题,可以设置国内镜像源重试。
提示:
npx playwright install失败是高频问题,多数情况下是下载浏览器内核时网络中断。可以多试几次,或者手动下载对应的 Chromium 版本放到缓存目录。
4.2 从文稿到音频的完整操作记录
我拿一份实际的 380 字文稿来演示。文稿内容是关于"如何用碎片时间做知识管理"的口播稿,分成 6 个自然段。
第一步,把文稿保存为script.txt,每段之间用空行分隔。
第二步,写一个简单的 Python 脚本批量合成音频:
import edge_tts import asyncio async def synthesize(text, output_file): communicate = edge_tts.Communicate( text, voice="zh-CN-YunxiNeural", rate="+8%", pitch="-2Hz" ) await communicate.save(output_file) async def main(): with open("script.txt", "r", encoding="utf-8") as f: paragraphs = [p.strip() for p in f.read().split("\n\n") if p.strip()] for i, para in enumerate(paragraphs): output = f"audio/part_{i:02d}.mp3" await synthesize(para, output) print(f"生成 {output}") asyncio.run(main())这个脚本会把每个段落单独合成一个 MP3 文件,放在audio目录下。
第三步,用 FFmpeg 把所有音频片段拼接成一个完整音频:
ffmpeg -f concat -safe 0 -i audio/list.txt -c copy audio/full.mp3list.txt的内容是每个音频文件的路径,格式如下:
file 'part_00.mp3' file 'part_01.mp3' file 'part_02.mp3'拼接完成之后,用ffprobe查看音频总时长:
ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 audio/full.mp3实测这份 380 字的文稿,合成音频总时长是141.8 秒,四舍五入就是 142 秒。
4.3 画面渲染与音画对齐的实操细节
音频时长确定之后,Remotion 的durationInFrames就定下来了:142 秒乘以 30fps,等于 4260 帧。
画面模板我做了两个版本:一个是静态标题版,适合整条视频只有一个核心观点的内容;另一个是分段切换版,适合有多个要点的内容。分段切换版需要在 Remotion 里用useCurrentFrame和interpolate控制每段内容的出现和消失时间。
import { useCurrentFrame, interpolate, AbsoluteFill } from "remotion"; export const OralVideo = ({ segments }) => { const frame = useCurrentFrame(); const fps = 30; return ( <AbsoluteFill style={{ backgroundColor: "#1a1a2e" }}> {segments.map((seg, index) => { const startFrame = seg.start * fps; const endFrame = seg.end * fps; const opacity = interpolate( frame, [startFrame, startFrame + 15, endFrame - 15, endFrame], [0, 1, 1, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" } ); return ( <AbsoluteFill key={index} style={{ opacity, justifyContent: "center", alignItems: "center", padding: "250px 80px", }} > <div style={{ fontSize: 72, color: "#fff", textAlign: "center" }}> {seg.text} </div> </AbsoluteFill> ); })} </AbsoluteFill> ); };segments数组里每个元素包含text、start、end三个字段,start和end是这段文字在音频中的起止秒数。这些数据可以从 edge-tts 生成的 SRT 字幕文件里解析出来。
渲染命令加上 props 参数:
npx remotion render OralVideo out/video_silent.mp4 --props='{"segments":[{"text":"第一段内容","start":0,"end":12},{"text":"第二段内容","start":12,"end":28}]}'渲染完成后,用ffprobe确认视频时长和音频一致。
4.4 最终合成与输出规格确认
最后一步是把无声视频、音频、字幕合成到一起。我习惯把这一步写成 shell 脚本,方便重复执行:
#!/bin/bash VIDEO="out/video_silent.mp4" AUDIO="audio/full.mp3" SUBTITLE="audio/full.srt" OUTPUT="out/final.mp4" # 第一步:合并音视频 ffmpeg -y -i "$VIDEO" -i "$AUDIO" -c:v copy -c:a aac -b:a 192k -shortest temp_with_audio.mp4 # 第二步:烧录字幕 ffmpeg -y -i temp_with_audio.mp4 -vf "subtitles=$SUBTITLE:force_style='FontName=Noto Sans SC,FontSize=18,PrimaryColour=&HFFFFFF,OutlineColour=&H000000,Outline=2,Alignment=2,MarginV=120'" -c:a copy "$OUTPUT" # 第三步:清理临时文件 rm temp_with_audio.mp4 echo "输出完成:$OUTPUT"执行完之后,用ffprobe检查最终文件的规格:
ffprobe -v error -select_streams v:0 -show_entries stream=width,height,r_frame_rate,codec_name -of default=noprint_wrappers=1 out/final.mp4确认输出是1080x1920,30fps,H.264,音频是AAC 192kbps,文件大小在 100MB 左右,符合主流平台的上传要求。
5. 常见问题与排查技巧实录
5.1 音频与画面不同步的三种排查方向
音画不同步是这套流程里最常见的问题,表现是字幕比声音快或者慢。排查方向有三个:
第一,检查音频总时长和视频总帧数是否匹配。用ffprobe分别查音频时长和视频时长,如果音频是 141.8 秒,视频是 142.0 秒,那 0.2 秒的差距在可接受范围内。如果差距超过 1 秒,就要检查 Remotion 的durationInFrames是不是算错了。
第二,检查 SRT 字幕的时间轴是否准确。edge-tts 生成的 SRT 有时候会有几毫秒的偏移,如果偏移累积到后面变得明显,可以手动调整 SRT 文件里的时间戳。
第三,检查 FFmpeg 合成时有没有用-shortest参数。如果不加这个参数,音频比视频长的时候,视频播完了音频还在放,最后就是黑屏加声音。
5.2 edge-tts 合成失败的常见原因
edge-tts 偶尔会合成失败,报错信息通常是网络超时或者音色不存在。我遇到过的原因和解决方法:
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 报错 voice not found | 音色名称拼写错误 | 用edge-tts --list-voices确认准确名称 |
| 合成到一半中断 | 网络不稳定 | 分段合成,每段单独重试 |
| 音频有杂音或断句奇怪 | 文本里有特殊字符 | 清理文本,去掉 emoji 和特殊符号 |
| 合成速度极慢 | 单次文本太长 | 把长文本拆成 80 字以内的短段 |
提示:edge-tts 是调用在线服务的,所以需要网络连接。如果网络环境不稳定,建议把文稿拆成更小的段,每段单独合成,失败的那段单独重试,不用全部重来。
5.3 Remotion 渲染报错与性能优化
Remotion 渲染报错最常见的是内存不足和字体缺失。内存不足的表现是渲染到一半进程被 kill,解决方法是降低--concurrency参数,减少并行渲染的帧数。字体缺失的表现是画面上的文字变成方块,解决方法是在项目里引入字体文件,或者在 CSS 里用系统自带的中文字体。
性能优化方面,我实测下来有几个有效的做法:
- 把
--concurrency设置为 CPU 核心数的 70% 左右,不要拉满,留出余量给系统。 - 画面里的动画尽量用 CSS transform 和 opacity,这两个属性可以被 GPU 加速。
- 如果画面是静态的,不需要逐帧渲染,可以用 Remotion 的
--frames参数只渲染关键帧,然后用 FFmpeg 补帧。
5.4 FFmpeg 字幕烧录的字体与位置问题
FFmpeg 烧录字幕时,如果系统里没有指定的字体,字幕会显示成默认字体,可能和设计稿不一致。解决方法是在force_style里用系统确实存在的字体名称,或者在系统里安装对应的字体文件。
字幕位置的问题主要是MarginV参数。这个参数控制字幕距离底部的距离,单位是像素。竖屏视频里,底部通常有平台的 UI 遮挡,所以MarginV建议设置在 100 到 150 之间。如果字幕被裁切了,检查一下视频的高度和字幕的Alignment设置是否匹配。
还有一个细节是字幕的换行。FFmpeg 默认不会自动换行,长句子会超出画面。解决方法是在 SRT 文件里手动插入换行符,或者用force_style里的WrapStyle参数控制换行方式。
6. 这套流程的扩展玩法与个人经验
6.1 批量生产的目录结构与脚本组织
当你要生产多条视频时,目录结构很重要。我用的结构是这样的:
project/ ├── scripts/ │ ├── synthesize.py │ ├── render.sh │ └── compose.sh ├── templates/ │ └── oral_video.html ├── contents/ │ ├── video_001/ │ │ ├── script.txt │ │ ├── audio/ │ │ └── output/ │ └── video_002/ │ ├── script.txt │ ├── audio/ │ └── output/ └── remotion-project/ └── src/每条视频一个独立目录,文稿、音频、输出都在自己的目录里,互不干扰。脚本放在scripts目录下,通过参数传入视频目录路径,这样一套脚本可以处理所有视频。
6.2 用 Playwright 做画面预览与自动化校验
Playwright 在这套流程里最大的价值是在渲染之前先看效果。Remotion 渲染一次要几分钟,如果画面有问题,重渲染的成本很高。用 Playwright 先把 HTML 模板在浏览器里打开,截图确认布局、字体、颜色都对了,再去渲染,能省很多时间。
const { chromium } = require("playwright"); (async () => { const browser = await chromium.launch(); const page = await browser.newPage({ viewport: { width: 1080, height: 1920 }, }); await page.goto("file:///path/to/template.html"); await page.screenshot({ path: "preview.png", fullPage: true }); await browser.close(); })();这段脚本会把 HTML 模板渲染成 1080x1920 的截图,你可以直接看到最终画面的效果。如果截图没问题,再去跑 Remotion 渲染,心里有底。
6.3 我踩过的坑与实操心得
第一个坑是音频采样率不一致。edge-tts 输出的 MP3 默认是 24kHz 采样率,而 Remotion 渲染的视频音频轨道是 48kHz。直接合并的时候,FFmpeg 会自动重采样,但有时候会出现轻微的音频失真。我的做法是在合成之前,先用 FFmpeg 把音频统一转成 48kHz:
ffmpeg -i audio/full.mp3 -ar 48000 -ac 2 audio/full_48k.mp3第二个坑是字幕文件编码。edge-tts 生成的 SRT 文件默认是 UTF-8 编码,但有些播放器或平台对 BOM 头敏感,带 BOM 的 SRT 会导致字幕显示乱码。解决方法是用sed去掉 BOM 头,或者用 Python 重新写一遍文件。
第三个坑是Remotion 的字体加载。Remotion 在渲染时是在 Node.js 环境里跑的,不是浏览器环境,所以 CSS 里的font-family如果引用的是系统字体,可能加载不到。解决方法是在 Remotion 项目里用@remotion/google-fonts加载网络字体,或者把字体文件打包进项目里用@font-face引入。
第四个坑是FFmpeg 的-shortest参数。这个参数在音视频时长接近的时候很好用,但如果音频比视频短很多,视频会被截断。我的做法是先确认音频和视频的时长差在 0.5 秒以内,再用-shortest,否则手动指定-t参数控制输出时长。
6.4 后续可以继续优化的方向
这套流程目前跑通了,但还有几个可以优化的点。一是画面模板的多样化,现在只有标题和正文两种布局,可以增加图文混排、数据图表、引用卡片等模板,让视频的视觉更丰富。二是背景音乐的自动混音,现在背景音乐是手动加的,可以写一个脚本根据音频的节奏自动匹配背景音乐的淡入淡出。三是多语言支持,edge-tts 支持多种语言,只要替换音色和文稿,同一套流程可以生产英文、日文等其他语言的视频。
我个人在实际操作中的体会是,这套流程最大的价值不是省了多少时间,而是把创作和制作分离了。你只需要专注于写文稿,剩下的全部交给脚本。文稿改一个字,重新跑一遍流程,两分钟后新视频就出来了。这种迭代速度,是手动剪辑完全做不到的。