把一份技术研究,做成一集 4 分钟的讲解视频
把一份技术研究,做成一集 4 分钟的讲解视频
前几天我想弄明白 RAGFlow 到底是什么,顺手做了个实验:不只写一篇研究文档,还要把它做成一条能发给非技术同事看的讲解视频。
结果这条链路比预想的长,也踩了不少坑。记录下来,也许对想做类似事情的人有用。
一、先想清楚给谁看
同一份材料,写给三个人看,得写三遍:
- 给技术决策者:关心架构、部署成本、和同类产品的差异
- 给开发者:关心怎么接、坑在哪
- 给完全不懂的人:关心"这东西解决了我什么麻烦"
我最后做了两个产物:一份 17 KB 的研究文档(给前两类人),一条 4 分 10 秒的视频(给第三类人)。
这个拆分很关键。如果只做一份,通常的结局是文档太浅、视频太深,两边都不满意。
二、资料:一手优先,镜像兜底
我给自己定的原则是:所有关键数字必须有出处,且出处不能是二手转述。
所以信息来源只有三类:
- 官网首页与产品页
- 官方文档(带版本号的那套)
- 仓库 README 与 release notes
有个细节值得说:我这个环境的 github.com 连不上(返回 502),但 cdn.jsdelivr.net 和 GitHub Pages 是通的。所以 README 是从 jsDelivr 的镜像路径取的,内容与仓库一致。
Star 数这类会变的指标,用 img.shields.io 的 JSON 接口读,比截图靠谱:
curl -s https://img.shields.io/github/stars/infiniflow/ragflow.json
# {"message":"93k", ...}另一个坑:官方文档存在 /docs/dev/ 和 /docs/v1.0.0-rc2/ 两套路径,前者会返回一个 283 字节的重定向壳页给你,看起来像"页面正常但没内容"。这类"HTTP 200 的假成功"最费时间——判断依据应该是内容长度,不是状态码。
三、把术语翻译成人话
技术内容讲给外行,难点不在简化,而在找到正确的类比。
我最后用了三个:
闭卷考试 → 开卷考试。 大模型像一个只背到训练那天的学生,公司内部资料它一份没见过。RAG 就是允许它答题前先翻书。
翻书比写字难。 这句话是整个视频的转折点。大多数人以为难点在"生成答案",其实在"找对页"。翻错了页,模型越聪明,错得越像真的。
切块的两难。 切得小,检索准但上下文断;切得大,上下文全但意思被稀释。没有哪个固定字数是对的——这个矛盾用天平式的左右对比一画就懂,用文字解释要三句话。
写口播稿时我给自己加了两条硬约束:
- 每句不超过 40 字,中间必须有换气点
- 不出现没有解释的英文缩写(RAG 除外,因为它必须出现)
第二条是我后来才意识到的重要:第一版稿子写得很"顺",但那是写给人看的顺,不是读给人听的顺。这两者差别很大。
四、出片:HTML 排版 → 截图 → ffmpeg
没有用剪辑软件,走的是纯代码路径:
HTML 版面(1920×1080)
↓ Chrome headless --screenshot
PNG
↓ ffmpeg(运动 + 字幕叠层)
无声视频
↓ 合并配音
成片选这条路的理由很实在:版式可以复用。16 屏共用一套 CSS,改配色只改一个变量,比在 After Effects 里逐帧调快得多。
坑一:crop 滤镜里的 t 是假的
我一开始想让每屏有轻微的缓慢推镜(Ken Burns 效果),写的是:
crop=w='floor(2688*(1-0.05*t/16)/2)*2':h='floor(...)':x='(iw-ow)/2':y='(ih-oh)/2'不报错,正常出片,看起来一切正常。
但 crop 的 width/height 只在初始化时求值一次,表达式里带 t 不会报错,只会静默失效。也就是说整条视频实际上是静态幻灯,一帧都没动。
我是怎么发现的?对比同一场景内相隔 10 秒的两帧,算 SSIM:
SSIM All: 0.9980.998 意味着几乎完全一致。如果真有 3% 的缩放,文字边缘会明显位移,不可能到 0.998。
改用 zoompan 才是对的,它是少数几个逐帧求值的滤镜:
zoompan=z='1+0.045*on/360':x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':d=1:s=1920x1080:fps=30改完再测,同场景 SSIM 降到 0.88——运动真实存在了。
这个教训挺通用的:默认滤镜是逐帧求值的,但 crop 的尺寸参数是例外。遇到"改了参数没反应还不报错",先怀疑求值时机。
坑二:这台机器的 ffmpeg 没有 drawtext
想加字幕,第一反应是 subtitles 或 drawtext 滤镜。查了一下:
ffmpeg -hide_banner -filters
# 没有 drawtext,没有 subtitles,没有 libass这个构建没编译文本渲染。
解决办法是把字幕也当成图层:单独渲染一张 1920×1080 的透明 PNG(只有底部一行字),再用 overlay 叠上去。
Chrome 出透明图需要两个条件:
--default-background-color=00000000
# 且 HTML 里 body 必须 background:transparent好处是字幕的字体、描边、渐变蒙版全部可控,比 libass 的样式能力强;坏处是每改一个字都要重新渲染。
坑三:时长要跟着配音走,不是反过来
第一版我先定死每屏 10 秒,再往里塞配音,结果有的屏话没说完就切了,有的屏说完了干等 4 秒。
正确顺序是:先生成配音,量出时长,再决定画面多长。
我的参数是每屏前留 0.55 秒、后留 0.75 秒,整轨用 loudnorm 归一到 -16 LUFS。这个留白量是试出来的,少了会显得赶,多了会显得拖。
五、配音:从系统 TTS 到云端 TTS
这部分是我被打回重做最多的地方。
第一版:macOS 自带的 say
say -v Tingting -r 172 -o out.aiff "文本"优点是零成本、离线、立刻可用。缺点是听起来就是"机器在念字"——断句硬,句尾下沉,完全没有语气。
我一开始觉得"能听就行",但把成片发给朋友看,第一反应是"声音好怪"。
更好的选择其实一直连得上
排查下来发现,我环境里的火山引擎密钥其实覆盖了语音合成(豆包语音合成 2.0,seed-tts-2.0),只是接口路径不在我以为的地方:
POST https://openspeech.bytedance.com/api/v3/plan/tts/unidirectional
X-Api-Key: <key>
X-Api-Resource-Id: seed-tts-2.0坑四:返回的是 NDJSON 流,不是 JSON
第一次拿到响应,看着像 JSON,但 json.loads 直接报错:
json.decoder.JSONDecodeError: Extra data因为它是多个 JSON 对象首尾相接的流:
{"code":0,"data":"SUQzBAAA..."}
{"code":0,"data":"...AAA//OEx..."}
...
{"code":20000000,"data":null}音频是 base64,被切成 20 多段分散在各个 chunk 里,最后以 code: 20000000 结束。正确做法是逐个 raw_decode、累加 data、再统一解码:
dec = json.JSONDecoder()
i, chunks = 0, []
while i < len(text):
obj, i = dec.raw_decode(text, i)
if obj.get("code") == 0 and obj.get("data"):
chunks.append(obj["data"])
if obj.get("code") == 20000000:
break
audio = base64.b64decode("".join(chunks))这个格式坑很典型:看着像 JSON 的,不一定是 JSON。单对象的 json.load 会成功解析第一个对象然后因为"多余数据"报错,这个报错信息很容易被误读成编码问题。
同一句话,五个音色的实测数据
45 个字的一句话(不含标点),测了时长和停顿:
| 音色 | 时长 | 字/秒 | 停顿占比 |
|---|---|---|---|
| 系统 Tingting | 10.72s | 4.20 | 12.7% |
| vv(通用女声) | 8.86s | 5.08 | 7.5% |
| 高冷御姐 | 9.46s | 4.76 | 27.4% |
| 爽快思思 | 8.04s | 5.60 | 5.3% |
| 甜美小源 | 8.64s | 5.21 | 18.6% |
| 阳光青年(男) | 7.22s | 6.23 | 13.4% |
两个结论:
一、云端音色普遍比系统音色快 20–48%。 快听起来利落,但讲解视频里会显得赶。
二、停顿占比比语速更能决定"像不像人"。 高冷御姐字/秒只有 4.76,但停顿占 27.4%——说得稳、停得住,是纪录片旁白的节奏。爽快思思正好相反(5.6 字/秒、5.3% 停顿),那是短视频口播,不是讲解。
如果你的目标是"讲清楚一件事",看停顿占比,别只看语速。
顺带一个坑:音色和资源包是绑定的。我试"北京小爷"那个音色直接报:
resource ID is mismatched with speaker related resource不是所有音色都能用在同一套资源下。
六、我学到的四件事
1. 素材的边界往往不是技术边界,是账号边界。 我一度以为"没有更好的 TTS",实际上接口一直是通的,卡在账号的一个开关上。遇到能力缺失,先确认是"做不到"还是"没开通"。
2. 静默失效比报错危险得多。 crop 带 t 不报错、/docs/dev/ 返回 200 但是空壳——这类问题不看结果只看状态码,永远发现不了。判断标准要落在内容上(SSIM、内容长度、音频时长),不是返回码。
3. 稿子的节奏比音色重要。 我把第一版稿子每句写 40–80 字,没有换气点,任何引擎念出来都赶。后来拆成短句、显式加停顿,即使还用系统语音,听感也提升明显。
4. 流水线要能重跑。 全程用脚本而不是手工操作,所以换配音这件事只需要重新生成音频、重跑一次合成,画面自动按新时长对齐。手工剪的话,这一改就是半天。
附:几个能直接抄的命令
远程抽视频片段(不下载整个文件):
ffmpeg -y -ss 3 -t 6 -i "https://example.com/demo.mp4" \
-vf scale=640:-2 -an -c:v libx264 -crf 30 preview.mp4前提是对方支持 HTTP Range。1.46 GB 的素材库,用这招抽 72 条预览只花了 6.7 MB。
判断两帧是否有真实运动:
ffmpeg -i a.png -i b.png -filter_complex ssim -f null /dev/null同一个镜头内,SSIM 应该明显低于 1;如果接近 0.999,说明你的"动画"其实没动。
检查音频电平:
ffmpeg -i out.mp4 -af volumedetect -f null /dev/null目标 mean_volume 在 -16 dB 附近,max_volume 不超过 -1 dB。
整个过程大概花了半天空闲时间,其中真正"创作"的部分(写稿、设计版式)不到三分之一,剩下都在解决工具链的脾气。这大概是所有自动化内容生产工作的常态。
