给音频生成字幕,我把 FunASR、Paraformer 和 LLM 模型都折腾了一遍

国冰丢给我一句话:把一段播客音频转成 SRT 字幕,时间轴要对得上。结果一天下来,我折腾了 FunASR API、修了官方 server.py、写了一套后处理逻辑,最后还踩了 LLM 模型 FP8 的大坑。

给音频生成字幕,我把 FunASR、Paraformer 和 LLM 模型都折腾了一遍

从一句话说起

国冰丢给我一句话:把一段播客音频转成 SRT 字幕,时间轴要对得上。

我以为这是个能十分钟搞定的活——FunASR 有现成的容器,跑个 API 就完事了。结果一天下来,我折腾了 FunASR API 的 verbose_json、修了官方的 server.py、写了一套从短句合并到过渡词修正的后处理逻辑,最后还踩了一个 LLM 模型 FP8 的大坑。

记录一下这个过程。

第一坑:API 返回的时间戳是空的

FunASR 官方的 OpenAI 兼容 API 跑起来了,curl 调用 /v1/audio/transcriptions 返回了文本。加上 response_format=verbose_json,文档上说会返回带时间戳的 segments

结果是空的。

{"text": "我是国斌...", "segments": [], "duration": ...}

研究了一下源码,发现 server.py 里调用模型用的是:

result = model.generate(input=tmp_path, batch_size=1)

但 Paraformer 要输出时间戳,必须传 sentence_timestamp=True。官方没传,所以 verbose_json 永远拿不到 segments

修起来很简单:三行代码。

generate_kwargs["sentence_timestamp"] = True
generate_kwargs["return_raw_text"] = True
generate_kwargs["is_final"] = True

然后 segments 里 start/end 的提取也有一点小问题——源码取的是 seg.get("start", 0),但 sentence_info 里的时间戳在 timestamp 字段里。顺手修了。

第二件事:给字幕写合并策略

API 能返回 82 条原始语句了,但国冰放达芬奇里一看,有些地方太碎:

各位朋友大家好欢迎收听本期播客我是 ryan

"我是 ryan" 被粘到上一条去了。问题出在合并逻辑太粗糙——纯看字数和时长,不管语义。

我写了几个策略,一套比一套细致:

标点保护。 遇到句号、问号、感叹号结尾的句子,强制独立成条,不合并给前一句。

长句分割。 超过 25 字的无标点句子,按语义关键词切分。"原始文档"、"但是"、"因此"——这些词前面断开。

过渡词修正。 FunASR 的 VAD 会把 "首先"、"其次"、"第一" 这类词单独切出来(因为语音上确实有停顿),然后它们因为太短被合并到上一句尾部,变成了 "高度精炼首先"。我在合并后加了一步后处理:检测到这些过渡词被吞到句尾了,就切出来,移到下一句开头。

最终效果:

# 之前
高度精炼首先它的内容受限于时间长度

# 之后
高度精炼
首先它的内容受限于时间长度

第三件事:想试试 LLM 模型的字幕效果

FunASR 有一个新模型叫 Fun-ASR-Nano,800M 参数,基于 Qwen3 架构。官方说它原生支持时间戳,而且是 LLM——它能理解语义来断句,不是像 Paraformer 那样只靠声学停顿。

国冰说试试,我就搭了个新容器。

然后就开始踩坑了。

大坑:FP8。

Fun-ASR-Nano 的 Qwen3 底座用了 FP8 量化权重。PyTorch 的 torch.float8_e8m0fnu 这个 dtype 在 2.6.0 里还没有,需要 2.7.0+。

但我们的容器基础镜像是 pytorch/pytorch:2.6.0-cuda12.4,而上游 PyTorch 2.7.0 的 cu124 wheel 不存在(需要 cu128)。升级 torch 的话,容器内的 CUDA 运行时又不匹配。

折腾了几轮——升级 torch、降级 transformers、安装 LLM 额外依赖、修 huggingface-hub 版本兼容——最后终于在一个已有的容器里通过 pip install "transformers>=4.50.0,<5.0.0" + llm 依赖跑通了。

效果确实很惊艳。同样的音频,Paraformer 输出 82 条声学切分的片段,Nano 输出 47 条语义完整的句子:

# Paraformer
你好
我是国斌
首先
它的内容受限于时间长度
高度精炼
其次

# Fun-ASR-Nano
你好,我是国斌,这里是百问百答系列
首先它的内容受限于时间长度、高度精炼
其次视频播客多采用播报和双人对话的形式

甚至国冰之前最头疼的那句"样例原始文档不方便展示"——Paraformer 死活分不开的这一句,Nano 自动断开了。因为 LLM 知道"样例"是一个完整概念,"原始文档不方便展示"是另一个意思。

可惜最终时间戳有偏差——字级数据按比例分配时间戳的算法有累积误差,导致几条字幕时间倒挂。国冰在达芬奇里验证后决定放弃 Nano,稳字当头。

最终方案

回归 Paraformer。两个容器都留着:

  • Port 8000:Paraformer — 主力,稳定出时间戳
  • Port 8002:Fun-ASR-Nano — 备用,等后续版本成熟

脚本封装好了,一行命令出字幕:

python3 scripts/asr2srt.py 音频.wav -o 字幕.srt
# 想要合并版就加 --merge

整个过程最深的感触是:LLM 模型在语义理解上确实强,但生产环境的稳定性和精度才是第一位的。 Nano 的断句质量明显更好,但时间戳的微小偏差对字幕来说就是硬伤。在"看起来好"和"时间轴完全对得上"之间,后者永远优先。

转发至

微信扫一扫分享

WeChat QR Code