完全自动化管线:TTS 旁白 → 精准字幕 → Playwright 录制 → FFmpeg 混音/字幕烧录/音量标准化
📑 流程目录
- [🛠️ 环境准备](#🛠️ 环境准备)
- [📝 阶段1 · 准备工作(旁白/音频/字幕)](#📝 阶段1 · 准备工作(旁白/音频/字幕))
- [🎬 阶段2 · 录制无声视频(Playwright)](#🎬 阶段2 · 录制无声视频(Playwright))
- [🎵 阶段3 · 混音(合并旁白+视频)](#🎵 阶段3 · 混音(合并旁白+视频))
- [🎞️ 阶段4 · 烧录字幕(FFmpeg 硬字幕)](#🎞️ 阶段4 · 烧录字幕(FFmpeg 硬字幕))
- [🔊 阶段5 · 音量标准化与防爆音](#🔊 阶段5 · 音量标准化与防爆音)
- [📖 FFmpeg 命令参数详解](#📖 FFmpeg 命令参数详解)
- [❓ 常见问题与解决方案](#❓ 常见问题与解决方案)
- [✅ 录制前/后专业检查清单](#✅ 录制前/后专业检查清单)
- [🔧 工具速查表](#🔧 工具速查表)
- [📌 总结与关键成功因素](#📌 总结与关键成功因素)
🛠️ 环境准备
必需工具安装
# edge-tts:微软免费神经网络语音,生成旁白音频
pip install edge-tts
# mutagen:精确读取MP3时长(毫秒级)
pip install mutagen
# playwright:无头浏览器录制HTML可视化动画为视频
pip install playwright
playwright install chromium
# imageio-ffmpeg:自动管理ffmpeg二进制,跨平台混音/字幕
pip install imageio-ffmpeg
标准目录结构
project_directory/
├── visualization.html # 可视化动画(含CSS+JS时间轴)
├── narration_script.md # 旁白文本草稿(分段书写)
├── subtitle_001.mp3 ~ NNN.mp3 # TTS逐条生成
├── narration_all.mp3 # concat拼接后完整旁白
├── subtitle_timings.json # mutagen测量每条真实时长
├── subtitles.srt # 自动生成SRT字幕
├── record_video.py / mix_audio.py / burn_subtitles.py / boost_volume.py
├── video_nosub/ # playwright原始webm
├── video_mixed.mp4 # 混音后(无字幕)
├── video_final.mp4 # 烧录字幕后
└── video_final_loud.mp4 # 音量扩大最终版
📝 阶段1:准备工作
目标:生成旁白音频 + 毫秒级精确SRT字幕。
Step 1.1 编写旁白文本
每段对应一个可视化场景,时长≤30秒,旁白文本与字幕内容100%一致。示例:
# 旁白脚本
## 第1段(0~5秒)
开场白文本。
## 第2段(5~12秒)
第二段旁白内容。
Step 1.2 生成旁白音频 (edge-tts)
推荐语音:zh-CN-YunxiNeural(男声沉稳) / zh-CN-XiaoxiaoNeural(女声自然)
edge-tts --text "文本" --voice zh-CN-YunxiNeural --rate "+0%" --write-media subtitle_001.mp3
批量生成脚本(基于已准备的文本列表):
import subprocess, json
with open('subtitle_timings.json', encoding='utf-8') as f:
texts = json.load(f)['subtitles_text']
for i, txt in enumerate(texts, 1):
subprocess.run(['edge-tts','--text',txt,'--voice','zh-CN-YunxiNeural','--write-media',f'subtitle_{i:03d}.mp3'])
Step 1.3 测量真实时长 (mutagen)
from mutagen.mp3 import MP3
import json, glob
timings = []
for mp3 in sorted(glob.glob("subtitle_*.mp3")):
timings.append(int(MP3(mp3).info.length * 1000))
data = {"subtitle_timings": timings, "subtitles_text": [], "total_duration_ms": sum(timings)}
with open("subtitle_timings.json", 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
Step 1.4 生成SRT字幕
import json
def ms_to_srt(ms):
h = ms//3600000
m = (ms%3600000)//60000
s = (ms%60000)//1000
mil = ms%1000
return f"{h:02d}:{m:02d}:{s:02d},{mil:03d}"
with open("subtitle_timings.json", encoding='utf-8') as f:
d = json.load(f)
durations, texts = d["subtitle_timings"], d["subtitles_text"]
cur = 0
lines = []
for idx, (dur, txt) in enumerate(zip(durations, texts), 1):
start, end = cur, cur+dur
lines.append(str(idx))
lines.append(f"{ms_to_srt(start)} --> {ms_to_srt(end)}")
lines.append(txt)
lines.append('')
cur += dur
with open("subtitles.srt", 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
Step 1.5 抽听验证多音字
逐条播放MP3,修正错误后重新生成对应音频。
🎬 阶段2:录制无声视频 (Playwright)
HTML要求 :字幕容器必须隐藏 (display: none),时间轴驱动推荐 setInterval(50ms)+performance.now() 并设置 VISUAL_LEAD_MS=250。
录制脚本 record_video.py
import asyncio, os, json, sys
from pathlib import Path
from playwright.async_api import async_playwright
if hasattr(sys.stdout, 'reconfigure'): sys.stdout.reconfigure(encoding='utf-8')
async def record():
out_dir = Path("video_nosub")
out_dir.mkdir(exist_ok=True)
for f in out_dir.glob("*.webm"): f.unlink()
with open("subtitle_timings.json") as f:
dur_sec = json.load(f)["total_duration_ms"] / 1000 + 2
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True, args=['--disable-gpu'])
context = await browser.new_context(viewport={'width':1920,'height':1080},
record_video_dir=str(out_dir),
record_video_size={'width':1920,'height':1080})
page = await context.new_page()
await page.goto(f"file://{os.path.abspath('visualization.html')}", wait_until='networkidle')
await asyncio.sleep(1)
# 验证字幕容器隐藏
hidden = await page.evaluate("() => window.getComputedStyle(document.getElementById('subtitleContainer')).display === 'none'")
if not hidden: print("⚠️ 字幕容器未隐藏")
await page.evaluate("window.startTimeline()")
print(f"📹 录制中... {dur_sec:.1f} 秒")
await asyncio.sleep(dur_sec)
await context.close()
await browser.close()
videos = list(out_dir.glob("*.webm"))
return str(videos[0]) if videos else None
if __name__ == "__main__":
video_path = asyncio.run(record())
print(f"无声视频: {video_path}")
🎵 阶段3:混音
3.1 合并旁白音频
# 生成filelist.txt
for f in subtitle_*.mp3; do echo "file '$f'" >> filelist.txt; done
ffmpeg -f concat -safe 0 -i filelist.txt -c copy narration_all.mp3
3.2 混音到视频
import subprocess, imageio_ffmpeg
def mix(silent_video):
ffmpeg = imageio_ffmpeg.get_ffmpeg_exe()
cmd = [ffmpeg, "-i", silent_video, "-i", "narration_all.mp3",
"-c:v", "libx264", "-c:a", "aac", "-shortest", "video_mixed.mp4"]
subprocess.run(cmd, check=True)
print("✅ 混音完成: video_mixed.mp4")
mix("video_nosub/recorded.webm") # 替换实际路径
🎞️ 阶段4:烧录字幕
import subprocess, os, imageio_ffmpeg
def burn(input_video="video_mixed.mp4"):
ffmpeg = imageio_ffmpeg.get_ffmpeg_exe()
srt_path = os.path.abspath("subtitles.srt").replace('\\', '/')
vf = f"subtitles='{srt_path}':force_style='FontSize=20,PrimaryColour=&Hffffff,OutlineColour=&H000000,BackColour=&H80000000,Outline=1,Shadow=0,MarginV=20'"
cmd = [ffmpeg, "-i", input_video, "-vf", vf, "-c:a", "copy", "video_final.mp4"]
subprocess.run(cmd, check=True)
print("✅ 字幕烧录完成: video_final.mp4")
burn()
🔊 阶段5:音量标准化
import subprocess, imageio_ffmpeg
def boost_volume(in_file="video_final.mp4"):
ffmpeg = imageio_ffmpeg.get_ffmpeg_exe()
out = in_file.replace(".mp4", "_loud.mp4")
cmd = [ffmpeg, "-i", in_file, "-af", "volume=2.0,alimiter=limit=0.95", "-c:v", "copy", out]
subprocess.run(cmd, check=True)
print(f"✅ 最终响亮视频: {out}")
boost_volume()
📖 FFmpeg 命令参数详解
通用参数
| 参数 | 说明 |
|---|---|
| -i file | 指定输入文件 |
| -c:v libx264 | H.264视频编码 |
| -c:a aac | AAC音频编码 |
| -c:v copy | 视频流直接复制(避免二次编码) |
| -shortest | 以最短输入流结束为止 |
常用视频滤镜 (-vf)
| 滤镜 | 用途 |
|---|---|
| subtitles=file.srt | 烧录SRT字幕 |
| scale=1920:1080 | 强制分辨率 |
音频滤镜 (-af)
| 滤镜 | 作用 |
|---|---|
| volume=2.0 | 音量放大2倍 |
| alimiter=limit=0.95 | 限制峰值防爆音 |
| loudnorm=I=-16 | EBU R128响度标准化 |
❓ 常见问题与解决方案
- 音画不同步 → 采用setInterval(50ms)轮询 + performance.now(),VISUAL_LEAD_MS微调(200~300ms),并用mutagen保证音频真实时长。
- 双层字幕 → 确保HTML中字幕容器display:none;录制前用JS验证隐藏状态。
- 多音字误读 → 同义替换或加入同音字注释,重新生成单条音频。
- Windows GBK编码错误 → 脚本开头添加
sys.stdout.reconfigure(encoding='utf-8')。
✅ 检查清单
📌 录制前必查项
- 旁白文本结构化(每段≤30s)且已排查多音字
- 旁白文本 = 字幕文本(100%匹配)
- 所有 subtitle_xxx.mp3 已生成
- mutagen 测量真实时长并更新 subtitle_timings.json
- subtitles.srt 已基于真实时长生成
- HTML 中 #subtitleContainer 样式为 display: none
- 录制脚本无额外sleep(页面启动等待除外)
- 可视化页面使用 setInterval+performance.now 驱动时间轴
- 已配置 VISUAL_LEAD_MS 偏移量
📤 输出前最终校验
- 混音阶段使用 -c:v libx264(确保视频流编码)
- 字幕烧录使用FFmpeg subtitles滤镜,样式清晰
- 验证字幕同步(播放video_final.mp4)
- 音量扩大或 loudnorm 标准化,防爆音
- 最终文件命名为 *_loud.mp4 便于区分
🔧 工具速查表
| 工具 | 用途 | 安装 |
|---|---|---|
| edge-tts | 高质量TTS(中文神经语音) | pip install edge-tts |
| mutagen | 读取音频时长 | pip install mutagen |
| playwright | 浏览器自动化录屏 | pip install playwright && playwright install chromium |
| imageio-ffmpeg | Python调用ffmpeg | pip install imageio-ffmpeg |
| ffmpeg | 混音/字幕烧录/音量 | 由imageio-ffmpeg自动提供 |
📌 总结与关键成功因素
五阶段自动化闭环: 准备(旁白+SRT) → 无声视频录制 → 混音 → 烧录字幕 → 音量标准化。
🔑 核心成功要素: mutagen绝对时长、setInterval时间轴驱动、VISUAL_LEAD_MS同步补偿、隐藏HTML原生字幕、FFmpeg滤镜烧录硬字幕。遵循本指南可达到工业级短视频批量生产质量。