AI News Factory 字幕同步方案详解(v2 — FunASR 逐句对齐)


一、为什么需要精确对齐?

1.1 问题背景

制作短视频时,字幕必须与配音精确同步。如果字幕出现得太早或太晚,观众会感到困惑。

v1 方案的问题

脚本文字 → 整段 ASR → 整段对齐 → 拆分超长字幕 → 多处大段空白

问题根源

1.2 v2 解决方案

脚本文字 → 语义拆句(8-15字) → FunASR字符级时间戳 → 逐句独立对齐 → 无间隙填充

核心改进

  1. 按语义拆成 8-15 字小句(保护专有名词)
  2. 每小句独立 ASR 对齐(一处失败不拖累整段)
  3. 前后锚点 + 字数比例填充未匹配字幕
  4. ensure_no_gaps 后处理(间隙 >500ms 自动延伸)

二、技术组件

2.1 TTS(MiMo V2.5)

2.2 ASR(FunASR paraformer-zh)

FunASR 输出格式

{
  "text": "高 老 夫 四 点 八 ...",
  "timestamp": [[190,390], [390,510], ...]
}

每个 token(可能包含多个字符)有精确的起止时间 ms。

2.3 字符级展开

将 FunASR 的多字符 token 展开为单字符列表,每个字符共享时间戳:

token "API" [1000ms, 2000ms] →
  char "A" [1000, 1333]
  char "P" [1333, 1666]
  char "I" [1666, 2000]

2.4 滑动窗口对齐

在展开的字符序列中,找到与脚本小句匹配的子序列,用匹配到的时间戳作为字幕时间。


三、完整工作流程

3.1 流程图

┌─────────────────────────────────────────────────────────────┐
│  输入:视频脚本(整段文字)                                    │
│  "Opus 4.8 正式发布,价格不变但 Fast Mode 降价三分之二..."      │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│  Step 1: 语义拆句(8-15 字小句)                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ "Opus 4.8 正式发布"          (9字)                    │   │
│  │ "价格不变但 Fast Mode"       (10字)                   │   │
│  │ "降价三分之二"                (6字)                    │   │
│  │ "输入从30美元降到10美元"      (11字)                   │   │
│  │ "输出从150美元降到50美元"     (12字)                   │   │
│  │ ...                                                  │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│  Step 2: TTS 配音                                           │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ scene3.wav (25.60s)                                  │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│  Step 3: FunASR 提取字符级时间戳                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ "Opus" [190ms, 510ms]                                │   │
│  │ "4"     [520ms, 600ms]                               │   │
│  │ "点"    [610ms, 730ms]                               │   │
│  │ "八"    [740ms, 860ms]                               │   │
│  │ ...                                                  │   │
│  │ 展开为字符级:                                        │   │
│  │   "O" [190,270] "p" [270,350] "u" [350,430]          │   │
│  │   "s" [430,510] "4" [520,600] ...                    │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│  Step 4: 逐句滑动窗口对齐                                   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 小句 "Opus 4.8 正式发布"                              │   │
│  │   → 在字符序列中搜索匹配 → startMs=190, endMs=2100   │   │
│  │                                                       │   │
│  │ 小句 "价格不变但 Fast Mode"                            │   │
│  │   → 搜索范围从上一句结束位置开始 → startMs=2200, ...  │   │
│  │                                                       │   │
│  │ 小句 "降价三分之二"                                    │   │
│  │   → 继续搜索 → startMs=4500, endMs=5200              │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│  Step 5: 填充未匹配字幕 + 无间隙后处理                      │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 未匹配字幕用前后锚点 + 字数比例填充                     │   │
│  │ 间隙 >500ms 自动延伸前一句 endMs                       │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│  输出:精确对齐的字幕文件                                    │
│  captions.json                                              │
│  [                                                          │
│    {"text": "Opus 4.8 正式发布", "startMs": 190,           │
│     "endMs": 2100},                                         │
│    {"text": "价格不变但 Fast Mode", "startMs": 2200, ...},  │
│    ...                                                      │
│  ]                                                          │
└─────────────────────────────────────────────────────────────┘

3.2 为什么用逐句对齐而非整段对齐?

对比 整段对齐(v1) 逐句对齐(v2)
ASR 匹配粒度 整段脚本文字 8-15 字小句
一处失败影响 整段后续全部错位 仅该句,其他不受影响
拆分时机 对齐后拆分(破坏时间轴) 对齐前已拆好(无需后拆)
间隙问题 大段空白(6s+) 无间隙(后处理保证)

四、关键代码

4.1 语义拆句

def semantic_split(text: str, min_chars=6, max_chars=15) -> list:
    """按语义拆成 8-15 字小句,保护专有名词"""
    # 1. 按句号/叹号/问号拆成大句
    # 2. 按逗号/顿号/破折号拆分
    # 3. 合并过短句,拆分过长句
    # 4. 保护英文专有名词不被拆断

4.2 FunASR 提取

from funasr import AutoModel

model = AutoModel(model="paraformer-zh")
result = model.generate(input="scene1.wav", batch_size_s=300)
# result[0]["text"]: "高 老 夫 四 点 八 ..."
# result[0]["timestamp"]: [[190,390], [390,510], ...]

4.3 逐句对齐

def align_single_sentence(sentence, expanded, search_start):
    """在字符序列中搜索小句的最佳匹配位置"""
    # 滑动窗口:从 search_start 开始
    # 允许跳过 ≤5 个不匹配字符(处理音译品牌名)
    # 匹配分数 > 0.25 且 > 25% 字符匹配即视为成功
    # 匹配分数 ≥ 0.7 提前终止搜索

4.4 无间隙后处理

def ensure_no_gaps(captions, scene_duration_ms, max_gap_ms=500):
    """如果相邻字幕间隙 > 500ms,将前一句 endMs 延伸"""
    for i in range(1, len(captions)):
        gap = captions[i]["startMs"] - captions[i-1]["endMs"]
        if gap > max_gap_ms:
            captions[i-1]["endMs"] = captions[i]["startMs"] - 50

五、实际效果对比

5.1 2026-05-29 视频数据

场景 v1 间隙 v2 间隙 改善
Scene 3(价格详情) 6.12s 空白 0.10s 消除
Scene 5(国产模型) 5.41s 空白 0.22s 消除
Scene 4(封号潮) 1.44s 空白 0.10s 消除
总字幕条数 ~50 条 64 条 +28%

5.2 字幕碎片对比

v1(拆分后产生碎片)

[40.84s] "Opus 4.8"           ← 0.5s 独立碎片
[41.35s] "正式发布,价格不变但" ← 被拆断

v2(语义拆句,完整表达)

[40.74s] "Opus 4.8 正式发布,"  ← 完整语义
[41.79s] "价格不变但 Fast Mode"  ← 完整语义

六、依赖安装

# FunASR(必须)
pip install funasr

# 首次运行会自动下载模型(~1GB)
# 模型: paraformer-zh(阿里达摩院)
# 缓存: ~/.cache/modelscope/hub/models/

七、常见问题

Q1: FunASR vs faster-whisper,选哪个?

特性 FunASR paraformer-zh faster-whisper
中文精度 业界最强 一般(需 large-v3)
时间戳级别 token-level word-level(需 medium+)
模型大小 ~1GB(自动下载) 39MB-3GB
速度 快(RTF 0.03) 慢(CPU)
安装 pip install funasr pip install faster-whisper

推荐:中文 TTS 字幕对齐用 FunASR,英文场景用 faster-whisper。

Q2: ASR 对齐失败怎么办?

自动回退到字数比例估算(fill_unmatched),用前后已匹配字幕的锚点分配时间。

Q3: 字幕太长/太短?

Q4: 如何提升对齐精度?

  1. 确保 TTS 音频质量清晰(无背景噪音)
  2. 语义拆句时保持专有名词完整
  3. FunASR paraformer-zh 已是中文最强模型

八、参考资源

转载请注明出处