三个小时的会我只想要一份能用的文字|FunASR 装完,我卡在断句上

上周四下午连着开了三个会,录音加起来四小时二十分钟。我的习惯是当天晚上把录音过一遍,摘出决议和待办,第二天早会直接用。这个习惯维持了大半年,靠的是一套自己拼的本地转写:先拿 whisper.cpp 跑一遍,再用脚本把结果切段,最后人工校对。
那天晚上校对到第二个小时,我关掉了窗口。
问题不是它听不懂,是它「听不懂的地方特别稳定地错在同一个方向」。我们内部把「合同履约」念快的时候,它给出「合同旅约」;同事说「这个季度的 kpi 只有华东达标」,它把 kpi 写成「KP I」,中间塞个空格,我按空格切词的后处理整句都对不齐;还有一段中英混着讲,「这个 case 我们 escalate 到法务了」转成了「这个 K 死我们 S 开了到法务了」。
这类错误最烦的是它不是随机噪声,是我每篇都要手工改一遍的高频词。一百句错八句,听着还有九成准确率,但落到「拿它当纪要底稿」这件事上,九成等于零——你必须逐句读,才敢确认哪句能用。
我试过三条路。换更大的 whisper 模型,同音字好一点,速度掉到 0.4 倍实时,一台 M2 跑四小时录音要十小时。走云上的接口,按分钟计费不贵,可会议内容里有客户名字、有没公开的排期,我不放心。第三条是自己训,看了一眼标注成本就放弃了。
所以当我翻到 FunASR 那句「中文 CER 只有 whisper 系三分之一」的说法时,第一反应是不信。达摩院这套东西我早有耳闻,2022 年底就开源了,一直以为是给学术界跑 benchmark 的,工程上不好用。这次我决定认真过一遍,理由是它最近的更新节奏变了:加了 OpenAI 兼容的 HTTP 接口、加了 llama.cpp 的单文件运行时、加了命令行工具,这些东西不是论文项目会做的事。
它其实是四颗小模型串成的一条流水线
我一开始以为 pip install funasr 装的是「一个模型」。读完 README 的模型清单才明白,它卖的是一套组件拼装的思路,模型可以自由换。
骨架上有四个位置:
| 位置 | 干什么 | 常用型号 | 参数量 |
|---|---|---|---|
| VAD | 找出哪几段有人说话 | fsmn-vad |
0.4M |
| ASR | 把语音转成字 | SenseVoiceSmall / Paraformer-zh |
234M / 220M |
| PUNC | 断句加标点 | ct-punc |
290M |
| SPK | 谁在说 | cam++ |
7.2M |
注意 VAD 那个 0.4M 和说话人那个 7.2M——整条链上最不值钱的两个,但缺了它们输出就没法用。没有 VAD,长音频会一次性喂进模型,超过三十秒就开始丢内容;没有 SPK,多人会议转出来是一坨没归属的文字。
组件化最直接的用法是一行装齐:
from funasr import AutoModel
model = AutoModel(
model="paraformer-zh",
vad_model="fsmn-vad",
punc_model="ct-punc",
spk_model="cam++",
device="cpu",
)
result = model.generate(input="meeting.wav", batch_size_s=300)
这条链跑完,result[0]["sentence_info"] 里是一段一段的结构化结果,每段带 start、end、spk、sentence 四个字段。这就是我要的东西:时间戳能定位到录音第几分几秒,spk 能区分是几个人在说话。
第二件我确认能用的是命令行。这个仓库 1.4 版之后把 CLI 做成了正经入口,setup.py 里挂了五个 console script:funasr、funasr-server、funasr-realtime-server、funasr-export、funasr-train。对我这种「一个文件夹里二十个 wav」的场景,CLI 比写脚本快:
# 最简:转一个文件,直接打印文字
funasr meeting.wav
# 带说话人和时间戳,输出 JSON
funasr meeting.wav --spk --timestamps -f json
# 批量出 SRT 字幕,写到 ./subs
funasr *.wav -f srt -o ./subs
# 指定模型和语种
funasr meeting.wav -m paraformer -l zh
第三件是把它变成本地 HTTP 服务,接口按 OpenAI 的音频转写格式来。我原本觉得这就是「蹭格式」,后来想明白它的价值:我手上那些笔记插件、自动化流程都只认一个 base_url,本地服务一起来,它们直接接上,两边代码都不用改。
funasr-server --host 127.0.0.1 --port 8000 --model sensevoice --device cpu
起好之后 POST /v1/audio/transcriptions,-F model=sensevoice,返回格式和云端那套一致。官方文档里专门写了一句提醒:这个服务默认不带鉴权,只该监听在 loopback 上。这话是对的,我见过太多人把这种服务开到 0.0.0.0 然后忘了。
还有一块我不太买账:情感和音频事件标签。SenseVoice 的输出会带 <|HAPPY|>、<|BGM|> 这类标记,rich_transcription_postprocess 负责剥掉。听着能统计「这场会负面情绪占比」,实际用途有限——一段话被 VAD 切成十几段,每段一个情绪标签,你得到的是十几个人工无法验证的判断。我的判断是,它在客服质检这种短句、单人、有明确业务规则的场景里可能有用,在长会议里是纯噪声。
同样偏噱头的还有一个:首页把「vLLM 加速」排得很显眼,但官方部署文档自己承认,vLLM 那条路只适用于 Fun-ASR-Nano 这种自回归模型,对 Paraformer 这种非自回归的不适用。想加速中文生产路径上最成熟的模型,这个方案跟你无关。
非自回归省下的时间,从时间戳里扣走了
FunASR 中文线的核心取舍,一句话能说完:用非自回归换速度。
whisper 是自回归的,一个字一个字往外吐,每个字都依赖前面已经吐出来的字。好处是上下文连贯、能直接输出翻译;坏处是慢,而且长音频容易「复读」——同一个句子循环输出,跑 whisper 的人应该都遇到过。
Paraformer 和 SenseVoice 走的是另一条路:一次前向,直接吐出整段字序列,不做逐字解码。这就是速度差的根本原因,不是工程优化做得好,是算法结构上少了一个循环。官方站点给的数字是纯 CPU 上约二十倍实时,我自己在 M2 上测短音频没那么好看(下面有具体数),但差一个量级的那个方向是对的。
代价有三个,而且都很实在。
第一,时间戳的粒度是粗的,而且固定粗。 我这次加了 --timestamps,拿到的是逐字时间段:一句话十九个字,十九个时间段,每段宽度正好 60 毫秒,一个不差。这不是巧合,是这类模型的输出粒度——token 一格 60ms。所以我知道每个字大概落在第几秒,但拿不到「从 62 毫秒开始」这种精度。做字幕够用,做「点某个词跳回录音那一帧」就糙了。要更细只能换 Paraformer 的 CTC 输出,或者上 Fun-ASR-Nano。
第二,跨段的上下文丢了。 VAD 把音频切成一段一段,每段独立送进 ASR。段与段之间没有上下文传递,所以一句话被 VAD 切在两段里的时候,前半段结尾和后半段开头各转一次,很容易各错一半。这也是为什么很多人在 issue 里抱怨「加了热词之后句子变短了」——热词替换是按段做的,段边界一变,替换的落点就跟着变。
第三,也是我认为最需要提前想清楚的:它的「准」是建立在标点模型和分段策略上的,不是建立在语言模型上的。 whisper 内部有语言模型在帮着纠正,所以你念错一个字它可能替你猜对;FunASR 这条链上,猜对的责任在 ct-punc 和后续替换规则。这意味着两件事:ct-punc 的版本会直接影响你的断句质量;热词功能是双刃剑。
这三条代价什么时候不可接受?要词级对齐、要翻译、要小语种,这三种别用它。翻译能力基本没有,官方清单里 whisper-large-v3 是被当作「一个可选模型」收进来的,不是对标对象;小语种那边最新的 MLT-Nano 覆盖 31 种语言,但中文圈之外的表现,官方自己也没给可比数据。
装之前先把 torch 这关过了
官方 README 给的安装步骤就两行,我照着走,中间在下载上栽了一次:
# 1. 先装匹配的 torch 和 torchaudio(这一步决定后面一切)
pip install torch torchaudio
# 2. 确认设备能被识别到
python -c "import torch; print(torch.cuda.is_available())"
# 3. 再装 funasr
pip install funasr
我第一次是直接连 PyPI 装的,pip install torch torchaudio 跑了二十五分钟,进程还在、缓存不涨、CPU 占用零点几——典型的连接挂在那里不动。杀掉换成清华源,三十六秒装完,torch 2.14.1、torchaudio 2.11.0。接着装 funasr 1.4.16,一分零八秒,顺带装了七十多个依赖。国内网络环境下这一步别硬等,换源是最省时间的动作。
第二步那个检查,在 Mac 上必然是 False,所以我没得选,只能走 CPU。这一步值得写下来,因为 AutoModel 对不可用的 CUDA 是静默回退的:写了 device="cuda" 不报错,安安静静在 CPU 上跑完,慢三倍你还以为模型就这水平。官方排障文档为此专门给了一段脚本,逐个打印 ASR、VAD、PUNC、SPK 四个模型参数实际所在的设备,注释写得很直白——「请求的设备不等于实际的放置」。这段我没在 CUDA 机器上验过,但它存在本身就说明踩的人够多。
pip check 也值得跑一次。依赖列表比看上去长:modelscope、huggingface_hub、transformers、hydra-core、omegaconf、umap_learn、oss2、kaldiio、librosa,umap_learn 是为了说话人聚类,oss2 是对象存储 SDK,hydra-core 是为了训练配置。只想转个音,却顺带装了半个训练框架。
硬件门槛这件事,官方没有给整机配置表。 我翻了 README、部署矩阵、排障文档,只找到一句「Python ≥ 3.8」,内存和显存的要求一个字没有。所以下面两档是我的推算,不是官方数字,推算依据我写清楚:
| 档位 | 配置 | 依据 |
|---|---|---|
| 最低能用 | 4 核 CPU / 8GB 内存 / 无独显 | SenseVoice 234M 参数按 fp32 约 0.9GB,加 VAD 和标点模型,权重总量 1.3GB 左右;纯文件转写不追速度够用 |
| 建议配置 | 8 核以上 / 16GB 内存 / 6GB 以上显存 | 显存这一档参考了第三方测评给出的 SenseVoice + VAD + SPK 常驻约 3.8GB 的数字,留一倍余量 |
那个 3.8GB 来自 andrew.ooo 的一篇第三方测评,不是官方数据,我当参考不当结论。同一篇里提到 faster-whisper 跑 large-v3 常驻约 2.4GB——这条链的显存占用反而更高,因为四个模型同时挂着。
磁盘这块我跑完才去看,数字有点意外。权重缓存 ~/.cache/modelscope 一共 2.0GB:ct-punc 1.1GB、SenseVoiceSmall 900MB、cam++ 28MB、fsmn-vad 3.9MB。
标点模型比识别模型还大。 想一下又合理:ct-punc 是 290M 参数、词表四十七万的模型,SenseVoice 只有 234M。只「听出字」一套 900MB;想让输出带标点、能当纪要读,磁盘和内存立刻翻倍。这就是「中文断句靠标点模型」的物理成本。
模型托管在 ModelScope;官方排障文档说中国大陆走 ModelScope 更快,境外可以传 --hub hf。境外访问 ModelScope 的 CDN 慢是长期问题。
我从命令行一路走到 JSON
端到端最省事的路径是 CLI。我用官方给的公开中文样例音频(asr_example_zh.wav,5.55 秒、16kHz 单声道)跑的,命令里把三个开关都打开:
funasr asr_example_zh.wav --spk --timestamps -f json -v
-v 必须加。不加你面对的是一段没有输出的沉默,不知道它在下载权重还是在推理。加上之后,stderr 会给你 Model loaded in 88.1s 和每一级的 rtf。
我这台 M2 上的真实耗时:
| 场景 | 墙钟时间 | 其中模型加载 | 纯推理 |
|---|---|---|---|
| 首次(要下载权重) | 95.12s | 88.1s | 1.243s |
| 二次(权重已缓存) | 13.02s | 约 11.5s | 1.528s |
5.55 秒音频、1.2 到 1.5 秒出结果,rtf 0.21 上下;stderr 里 ASR 那一级的 rtf 是 0.016,差一个量级——整条链还要跑 VAD 和说话人聚类,这两步耗时固定,音频越短摊得越难看。
识别结果和官方 README 里写的那句样例文本一致:欢迎大家来体验达摩院推出的语音识别模型。十九个字,一个没错。
输出的 JSON 是这样(我删掉了中间十七个时间戳):
{
"text": "欢迎大家来体验达摩院推出的语音识别模型",
"segments": [
{
"start": 610,
"end": 5530,
"text": "欢迎大家来体验达摩院推出的语音识别模型",
"timestamp": [[940, 1000], [1120, 1180], "……共 19 组……"],
"speaker": 0
}
],
"file": "asr_example_zh.wav",
"model": "sensevoice",
"language": "auto",
"audio_duration_s": 5.547,
"processing_s": 1.243
}
三个细节。start、end、timestamp 里的数,单位是毫秒。speaker 是录音内部编号,从 0 开始,换文件重新排,不代表某个具体的人——官方文档写得很死,「录音内匿名标签,不是已注册说话人识别」,想做「这是张经理」得自己加一层声纹注册,cam++ 只给嵌入向量。processing_s 是纯推理耗时,拿它算成本比墙钟准。
然后是这一趟里我最想让人提前知道的一件事:--spk 这个开关,命令行并没有顺手把标点模型带上。 我的 stderr 里躺着这么一行:
[WARNING] punc_model is missing, falling back to vad_segment mode for speaker diarization.
意思是说话人分离退回了「按 VAD 段判定」——只知道这几段之间有停顿,不知道标点在哪,于是说话人切换点只能落在停顿上。两个人抢话、或者一个人说完另一个马上接,这一段就会被判成同一个人。想要干净的切换,得在 Python 里显式挂上 punc_model="ct-punc",CLI 没给你这个口子。这条是我自己撞出来的。
顺带两个小提示。一是启动时会告诉你 ffmpeg is not installed. torchaudio is used to load audio——没装不会失败,只是换 torchaudio 读音频,mp3、m4a 兼容性差一点。二是 SRT 输出,funasr asr_example_zh.wav -f srt -o ./subs 给我的是单条字幕:
1
00:00:00,940 --> 00:00:04,900
欢迎大家来体验达摩院推出的语音识别模型。
注意它自动补了个句号,也把起始时间对到了第一个字的 940 毫秒,而不是从 0 开始——这个细节做得挺对,很多工具会从 0 起。
想要更像会议纪要的排版、并且把 SenseVoice 的 <|HAPPY|> 这类标签剥掉,只能回到 Python:
from funasr import AutoModel
from funasr.utils.postprocess_utils import rich_transcription_postprocess
model = AutoModel(
model="iic/SenseVoiceSmall",
vad_model="fsmn-vad",
punc_model="ct-punc", # 不加这个,说话人切换点会很粗
spk_model="cam++",
device="cpu",
)
result = model.generate(input="meeting.wav", batch_size_s=300)
for seg in result[0]["sentence_info"]:
print(f"[{seg['start']/1000:.1f}s] 说话人{seg['spk']}: "
f"{rich_transcription_postprocess(seg['sentence'])}")
这里 sentence_info 的字段是 start、end、spk、sentence,和 CLI 的 segments 不是一套命名——同一个项目,两条入口两套结构。batch_size_s=300 是「一次拼三百秒音频送进模型」,不是批次数,调大有吞吐收益、代价是内存线性上升;这个参数我没在长音频上压过,旋到多少得看你的机器。
流式那条路是另一套东西,别混着用。它需要专门的流式模型,代码长这样:
import soundfile as sf
from funasr import AutoModel
model = AutoModel(model="paraformer-zh-streaming", device="cpu")
audio, sr = sf.read("speech.wav", dtype="float32") # 必须 16kHz 单声道
chunk_size = [0, 10, 5] # 600ms 一块
stride = chunk_size[1] * 960
cache = {}
n = (len(audio) - 1) // stride + 1
for i in range(n):
chunk = audio[i*stride:(i+1)*stride]
res = model.generate(input=chunk, cache=cache, is_final=(i == n-1),
chunk_size=chunk_size,
encoder_chunk_look_back=4, decoder_chunk_look_back=1)
if res[0]["text"]:
print(res[0]["text"], end="", flush=True)
两个 look_back 是精髓:encoder_chunk_look_back=4 是「编码器往前看四块」,decoder_chunk_look_back=1 是「解码器往前看一块」。往前看得多,边界处的字就准,代价是延迟变高。[0, 10, 5] 是六百毫秒一块,有人嫌它对直播太保守,想更低延迟就把块切小,准确率会掉。这旋钮没有标准答案。
issue 区里翻到的六件事
公开讨论这块,中文圈谈 FunASR 的文章不少,多数是「速度快、中文准」的复述。我挑几个有信息量的说,都能点进去核。
热词会让句子变短。 这是 issue 区讨论最多的一个,编号 #3302,34 条评论,从 7 月开到今天没关。提问的人描述得很具体:实时服务里,预览(partial)能看到的文字,到最后确认(final)那版会少掉一大截;热词加得越多丢得越厉害,还出现过「静音状态下冒出热词里的字」。我的理解是热词替换发生在分段结果上,替换一旦改变句子结构,边界就跟着重算,重算就会吃掉内容。这条至今没有官方结论,我的建议是离线用热词,实时别用。
GGUF 的 Vulkan 后端在 AMD 卡上崩。 #3479,22 条评论,Windows 11 + RX 7600M XT,--backend vulkan 在设备检测之后立刻 0xc0000005 访问冲突,拿 sensevoice-small-f16.gguf 能复现。官方在 issue 里要求提供 nvidia-smi,可这是 AMD 卡,本来就没有——模板化回复,算是团队工程习惯上的一个粗糙面。
实时服务达不到实时。 #3038,16 条评论,环境是 L20 单卡,测试方法是浏览器开多个 demo 同时放 B 站长音频,结论是「播得越久延迟越大」。这个现象很典型:不是单块推理慢,是缓存和队列在累加。想上实时字幕的人,这条必读。
CLI 和代码跑出来的结果不一样。 #3773,10 月 14 日提的。提问的人发现同一个音频、同一版 1.4.16,用 funasr 命令跑出来的断句比 Python 里调 AutoModel 更合理。我顺着看了源码:cli.py 生成字幕时会调 jieba 分词,按词边界决定在哪里断开;没有 jieba 就退化成只按标点断,代码里会打 warning 说「可能把词切到两行去」,10 月 27 日有个提交(#3726)专门给这个降级加提示。不过 jieba 是默认依赖,pip install funasr 装完就有,所以大多数人看不到这条 warning——真正会退化的,是手工裁依赖的精简镜像。纯文本输出两条路有没有区别,我没逐句比过,不下结论。
权重许可和代码许可是两件事。 仓库是 MIT,但模型权重另有许可。#3458 这条问的就是「四个固定 revision 的模型在自托管和权重分发场景下到底适用什么条款」,回答并不干脆。如果你的场景是把权重打进私有部署包里发给客户,这一条必须自己找法务确认,别看到 MIT 就以为整条链都能随便分发。
还有一条对做时间戳的人很致命:#3496,Fun-ASR-Nano 的 Hugging Face checkpoint 缺了 86 个 CTC 张量,时间戳和说话人分离那条路走不通。-hf 那个权重和 FunASR 版不是等价的包,选错一个功能就少一块。
外部视角也找了几个。funasr.com 官网有一篇自家做的对比,184 段中文音频、同机 CPU、字符级 CER:SenseVoice 8.0%、Paraformer 9.9%、Fun-ASR-Nano 8.3%,whisper 系(small/base/large-v3-turbo)在 22% 到 31% 之间。官方自测,口径对它有利,我当「方向可信、绝对值打八折」。hysenlabs.com 有一篇偏工程的解读,提到两个我没注意的 extras:funasr[silero] 换 VAD 后端,kaldi-native-fbank 是给没有 torchaudio 轮子的昇腾、aarch64 服务器兜底——这两个 extra 的存在说明,它的默认假设是「一台正常的 x86 + torchaudio 机器」。
谁该搬,谁别动
值得搬的:录音要留在自己机器里的(会议、访谈、客服、医疗);中文或中英混说为主的;需要「谁在什么时候说了什么」这种带说话人和时间戳的结构化输出;手上已有一堆工具、只缺一个本地 base_url 的——这一类收益最大,接上就能用。
别动的:要小语种和翻译的,去用 whisper 系;要词级精确对齐做逐字字幕的,这不是它强项;显存只有 4GB 还想挂 VAD + 说话人的,会难受;以及最重要的——要「装完就能用、出错有人管」的,别碰。这是个工具包不是产品,它的文档会明确写「跑通不代表结果可信」,剩下全是你自己的判断。
替代方案,我拿两个比:
faster-whisper(SYSTRAN/faster-whisper,26,235 star)。它是 whisper 的 CTranslate2 重写版,维度差别在多语言与生态:99 种语言、能翻译、社区大、踩坑资料多,代价是中文 CER 高出两三倍、显存常驻反而更低但速度慢得多。选它的理由只有一个:你的音频不是中文为主。
whisper.cpp(ggml-org/whisper.cpp,54,713 star)。维度差别在部署形态:一个二进制、零 Python、Mac 上还能吃 Metal。FunASR 也做了对应的 GGUF 运行时,但官方 release 里我只看到 Linux x64 的 Vulkan 包和 Windows 的 Vulkan/CUDA 包,没有 macOS 的——想在 Mac 上跑单文件版,这条路暂时走不通。这也是我留在 Python 版的原因。
一句话:FunASR 治的是「中文听不准」,不治「装起来麻烦」。
总而言之,它值得我换掉原来的 whisper.cpp 流水线,代价是要接受它的形状:四颗模型、一堆依赖、一堆自己调的旋钮。顺手看了下仓库,现在是 20,845 个 star、2,095 个 fork、58 个待处理 issue,最近四周 43 次提交,最新发版 v1.4.16 是 9 月 18 日,release note 里写的是修掉「VAD、标点、说话人模型的设备放置跨调用不保持」这类问题——都是很具体的工程债。
最后说句实在的,这一趟我只验到「5.55 秒的样例能干净地跑出带时间戳和说话人的 JSON」。我手上那几个小时的真实会议录音不方便往任何环境里传,所以长音频的稳定性和抢话时的分离效果,我一个都没测;GPU 吞吐更没测,因为我没有能开的机器,上面的显存数字全是别人的,出处我标了。想判断它能不能替掉你现在的方案,只有一个办法:挑二三十段跟业务同款的音频,两边各跑一遍,自己听。这个项目最让我放心的地方也在这儿——它不承诺结果,只承诺接口。