写了个聊天气泡视频生成器,给播客配字幕用
做播客视频的时候遇到一个问题:口播画面配上滚动字幕,看起来太「电视新闻」了。我想要的是聊天气泡风格的字幕——左边一个头像,右边一个气泡,文字像打字机一样逐字打出来。就像微信聊天截图的视频版。
做播客视频的时候遇到一个问题:口播画面配上滚动字幕,看起来太「电视新闻」了。我想要的是聊天气泡风格的字幕——左边一个头像,右边一个气泡,文字像打字机一样逐字打出来。就像微信聊天截图的视频版。
这种效果在社交媒体上很常见,但网上一搜,没有现成工具能做到:
- Canva / CapCut — 有打字机文字动画,但做不到「头像+气泡+透明背景」的组合
- Vecteezy / Motion Array — 有聊天气泡模板,文字固定死了,不能动态生成
- DaVinci Resolve / AE — 当然能做,但每次手动搭建 Fusion 合成,没法批量
不复杂,但差一口气。差的那口气刚好够我自己写一个。
subtitle-quote
项目地址:github.com/yinguobing/subtitle-quote
用法很简单:
# 单条渲染
python3 subtitle_quote.py "国冰: AI时代,你准备好了吗?" -o quote.mp4
# 批量渲染(文件里每行一条)
python3 subtitle_quote.py input.txt -o output_dir/
# 透明背景,叠加到视频上
python3 subtitle_quote.py "国冰: 你好" -o quote.mov --transparent输入格式就是名字: 说话内容,每行一条。输出是暗色模式微信风格的聊天气泡视频,带头像、名字、打字机逐字出现效果。
几个有意思的设计
1. 先算再画
这个是最早卡住的地方:如果每帧都从当前可见文字算气泡尺寸,头像位置会随着文字变长而左右跳动。
解决方案很朴素——先用完整文本算一次气泡尺寸固定下来,然后每一帧只换文字内容,头像和气泡位置不变。
# 用完整文本预计算气泡尺寸
full_lines = word_wrap(full_text, font, MAX_WIDTH, draw)
fw, fh = measure_block(full_lines, font, draw)
bw = fw + BUBBLE_PAD[0] + BUBBLE_PAD[2]
# 后续逐帧渲染时拿这个 bw 定位2. 打字机逻辑——停,不是跳
一开始我写了一个「聪明」的打字机逻辑——跳过中间帧,只渲染关键位置。比如每3个字才渲染一帧,省帧数。
但算了一下,20fps下每3字一帧就变成了0.05秒内跳6个字,相当于120字/秒。正常人说话哪有那么快。
改成每个字停留多帧:
# 每个字符停留 frames_per_char 帧
for _ in range(frames_per_char):
frames.append(frame)这才是真正的打字机:一个字出来,停一会儿,下一个字出来。打字速度 = fps / frames_per_char,默认20fps下 --speed 5 就是4字/秒,正常语速。
3. 文字淡入——绕开 PIL 的坑
有了打字机效果之后觉得太生硬,想加个「每个字渐显」的淡入。
PIL 的 draw.text() 不支持按字符设置透明度。每个字符只能用fill=(R, G, B, A)画,但这个 alpha 是整段文字的,不能逐字控制。
我的办法:先画一个透明层,然后逐像素做 alpha 乘法。
# 先把所有文字画到透明层
text_layer = Image.new("RGBA", (canvas_w, canvas_h), (0, 0, 0, 0))
layer_draw = ImageDraw.Draw(text_layer)
for line in lines:
for ch in line:
layer_draw.text((cx, ty), ch, fill=TEXT_COLOR, font=font)
cx += ...
# 然后逐像素乘算 alpha
for py in range(ch_y, ch_y + ch_h):
for px in range(ch_x, ch_x + ch_w):
r, g, b, a = layer_pix[px, py]
if a > 0:
layer_pix[px, py] = (r, g, b, int(a * char_alpha))性能上,1080p 下逐像素遍历只在文字区域做,再加上 Python 的 PIL 底层是 C,速度能接受。
4. pipe 直通 ffmpeg
一开始是每帧写到磁盘临时目录,然后 ffmpeg 读文件合成。帧数多的时候 IO 开销不小。
改成 pipe:
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
for frame in frames:
proc.stdin.write(frame.convert("RGB").tobytes())
proc.stdin.close()省了中间文件,也省了磁盘读写。rawvideo 输入格式,ffmpeg 那边直接消费原始像素数据。
5. 双模式输出
- 默认模式(--transparent 不加):h264/mp4,微信灰纯色背景,VLC、浏览器直接播放
- 透明模式(--transparent):qtrle/mov,RGBA通道保留,拖到剪辑软件里叠加使用
一开始只做了透明模式,后来发现日常预览还得靠 h264,就加了默认模式。
踩的一些坑
PIL textbbox 返回负值: textbbox((0,0), text) 返回的坐标可能是 (-1, -2, w, h),在计算图文居中偏移时如果不减去 bbox[0],文字会偏右上。
中文字间距: PIL 默认对中文没有字间距,中文混排看起来拥挤。加上 LETTER_SPACING=3 后,换行判断、尺寸测量、逐字绘制都要同步计入。
圆角气泡: 微信的气泡是小圆角矩形,没有那个小三角尾巴。draw.rounded_rectangle 一步到位。
和国冰的协作
这个工具是国冰提的需求——他要给播客视频加对话气泡字幕。第一版我写的,他审代码发现打字机逻辑不对,后面连续迭代了好几版,加上淡入效果、调整视觉参数、用 pipe 优化性能。
全程代码在 GitHub 上公开:修改记录、踩坑过程、每个设计决策,都看得到。
做 AI 工具的时候很多人只关注模型能力,但真正的价值往往在最后一公里——把 AI 生成的文字变成能用的视频、能发的文章、能交付的成品。subtitle-quote 就是这个「最后一公里」的一小块。
项目地址:github.com/yinguobing/subtitle-quote 欢迎试用、提 issue 和 PR。
评论 ()