> ## Content Index
> Fetch the complete content index at: https://yinguobing.com/blog/llms.txt
> Use this file to discover other available public pages before exploring further.

# 写了个聊天气泡视频生成器，给播客配字幕用
- URL: https://yinguobing.com/blog/chat-bubble-video-generator/
- Published: 2026-06-10T10:36:22.000Z
- Updated: 2026-06-12T12:04:13.000Z
- Description: 做播客视频的时候遇到一个问题：口播画面配上滚动字幕，看起来太「电视新闻」了。我想要的是聊天气泡风格的字幕——左边一个头像，右边一个气泡，文字像打字机一样逐字打出来。就像微信聊天截图的视频版。
- Author: 小黑
- Tags: 开源工具

做播客视频的时候遇到一个问题：口播画面配上滚动字幕，看起来太「电视新闻」了。我想要的是**聊天气泡风格的字幕**——左边一个头像，右边一个气泡，文字像打字机一样逐字打出来。就像微信聊天截图的视频版。

这种效果在社交媒体上很常见，但网上一搜，没有现成工具能做到：

- **Canva / CapCut** — 有打字机文字动画，但做不到「头像+气泡+透明背景」的组合
- **Vecteezy / Motion Array** — 有聊天气泡模板，文字固定死了，不能动态生成
- **DaVinci Resolve / AE** — 当然能做，但每次手动搭建 Fusion 合成，没法批量

不复杂，但差一口气。差的那口气刚好够我自己写一个。

## subtitle-quote

项目地址：[github.com/yinguobing/subtitle-quote](https://github.com/yinguobing/subtitle-quote?ref=yinguobing.com)

用法很简单：

```bash
# 单条渲染
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\. 先算再画

这个是最早卡住的地方：如果每帧都从当前可见文字算气泡尺寸，头像位置会随着文字变长而左右跳动。

解决方案很朴素——先用完整文本算一次气泡尺寸固定下来，然后每一帧只换文字内容，头像和气泡位置不变。

```python
# 用完整文本预计算气泡尺寸
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字/秒。正常人说话哪有那么快。

改成**每个字停留多帧**：

```python
# 每个字符停留 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 乘法**。

```python
# 先把所有文字画到透明层
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：

```python
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](https://github.com/yinguobing/subtitle-quote?ref=yinguobing.com) 欢迎试用、提 issue 和 PR。