FunASR 语音识别本地部署实录(下):34 分钟录音实测,踩过的坑、上线前要补的事

原文链接:FunASR 语音识别本地部署实录(下) 34 分钟录音实测,踩过的坑、上线前要补的事

服务能 curl 通只是开始。一段 63MB、34 分钟的双轨录音丢进去,才是真考验。

上一篇把服务搭起来了,这篇拿一段真实录音压测。过程里先撞了 413,又看着 RTF 从 0.24 一路涨到 2.72,顺手把上线前要补的事列了张清单。文里所有数字都是我自己这台 Mac 上跑出来的,换台机器会有出入,但量级和结论大差不差。

一、第一次撞墙:413 file too large

测试用例是一段双轨立体声录音,63MB,34 分钟。请求发过去,返回 {"detail":"file too large (>25MB)"}

问题不在模型,在我自己。上传上限沿用了 OpenAI Whisper API 的默认值 25MB,这个数字在单声道、16kHz、1 小时以内的录音上成立,但我这份是双轨立体声,同样时长文件体积直接翻倍,63MB 就是这么来的。

修法很简单:上限改成环境变量注入,默认 200MB,部署的时候写进启动脚本,代码一行不用动。重启服务,请求顺利进入推理阶段。25MB 这个坑我估计大部分自建的人都会撞一次,它藏在配置里,文档不会提醒你。当时请求长这样:

|------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| curl http://127.0.0.1:8000/v1/audio/transcriptions \ -F file=@/path/to/R8002_M8002_N_SPK8005.wav \ -F model=paraformer -F response_format=verbose_json |

注意 response_format 我传的是 verbose_json,为的是拿 segments 时间戳,纯转文字用 json 就够了,响应体积还能小一点。

二、34 分钟录音的实测数据

先解释一下 RTF,Real-Time Factor,处理耗时除以音频时长,1 就是实时,越小越快。0.56 意味着处理 1 秒音频要花 0.56 秒,也就是 34 分钟的录音,约 19 分钟出结果。

第一次跑,机器上没有其他重负载,2062 秒的音频用了 1159 秒处理,RTF 0.56,约 1.8 倍实时。这个数字离官方宣传的"120 倍实时"差得远------那是 GPU 加理想条件的说法,我这是 CPU,再叠加双轨解码、macOS 内存带宽、Python GIL 这些因素,实际值低一截我一点都不意外。

真正让我上心的是并发。机器上跑着其他重负载时再请求一次,处理时间直接飙到 90 多分钟,RTF 2.72。不是模型变慢,是 CPU 周期被邻居进程分走了。所以生产上这条要提前想清楚:ASR 要么独占机器,要么用 nice 限核,要么排队限并发。我预期上线后主要瓶颈就在并发上,单条长音频反而是好处理的。

verbose_json 返回的 segments 数组里,每段带毫秒级起止时间和文本,时间戳误差在百毫秒级,做字幕对齐够用。标点是 CT-PUNC 按口语停顿还原的,读起来像人话,不是机器直出的词串。RTF 这个指标我现在所有压测、容量评估、告警阈值都拿它当基准,建议自建 ASR 的人也记下来。官方代码里还有个 batch_size_s 参数,控制一次喂给模型多少秒的音频,改大一点吞吐通常能挤出来一些,我没细调,默认值够用,等有空再试。

三、性能边界,心里要有数

场景 音频时长 处理耗时 RTF
官方示例 5.55s 1.31s 0.24
1 分钟会议片段 60s 4.47s 0.07
34 分钟双轨立体声 2062s 1159s 0.56

三组数据放一起看,我心里大概有了数:30 分钟级的音频这台机器能稳跑,1 小时级要花接近半小时去处理,体验就不太行了。注意 1 分钟片段那行 RTF 最低,那是短音频 VAD 切段少、拼接开销小,别拿它跟长音频横向比。我给自己定的判断标准很简单:RTF 小于 1 是赚的,大于 1 就要考虑优化。

我现在的用法是把它当异步任务:录音丢进去,该干嘛干嘛,回头来取结果。1 小时以内的音频,这个节奏完全能接受;真到 2 小时以上,我估计这台机器就吃力了,得换思路。

真到了 1 小时以上,我给自己备了三条路:按时长切片并行、换带 GPU 的工作站、或者直接上官方流式方案。paraformer-zh-streaming 支持 600ms 一包的低延迟输出,会议转写、直播字幕这类场景,流式反而比离线合适。我现阶段还是第一种节奏,异步排队,回头取结果,真到处理不过来的那天,我大概率会先试切片并行,毕竟不用花钱。

四、踩过的坑

先说装包阶段的三个。默认 PyPI 源慢,带 C 扩展的包半天拉不动,切清华镜像解决;解释器路径不一致,shell 里找不到包,改用绝对路径;模型首次下载一个多 GB,得先预热缓存目录,别等线上第一次请求才现场下。

运行阶段也有三个。后台进程会被 shell 连带杀掉,macOS 上老老实实用 launchd 守护,配 KeepAlive 挂了自动拉起,日志统一走系统日志,比 nohup 加 & 稳得多;413 上传超限,把上限做成环境变量;CPU 被抢 RTF 飙到 2.7,ASR 要么独占机器要么限核。

对外暴露两个。直开 8000 端口没鉴权,裸奔到公网等于送人肉鸡,前置 Nginx 或 Caddy 网关;健康检查要接入负载均衡,不然扩容缩容都没依据。

质量两个。业务术语识别错,用 hotword 参数注入热词,官方 README 里就有例子;没有说话人归属,启动加 spk_model="cam++",7.2M 参数的小模型,代价很小,会议纪要场景立刻值回票价。

把这十个坑拢一拢:环境占了六个,模型本身反而很省心。以后真踩到问题,我打算先怀疑环境,再怀疑模型。

五、补上说话人分离:cam++ 实测,11 个人的会议怎么排出来

坑里留了句"启动加 spk_model=cam++,代价很小",光说不练假把式。这一节把这一行配置背后的账算清楚,再把同一段 34 分钟录音的说话人分离结果摆出来。

5.1 配置和代价

cam++ 的全名是 iic/speech_campplus_sv_zh-cn_16k-common,ModelScope 上下下来 12 个文件,主权重 campplus_cn_common.bin 不到 90MB,加上几个配置和示例音频,整体跟一个向量化召回模型差不多大。funasr 这边挂载的方式也很轻:

|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| model = AutoModel( model="paraformer-zh", vad_model="fsmn-vad", punc_model="ct-punc", spk_model="iic/speech_campplus_sv_zh-cn_16k-common", device="cpu", ) |

启动时间从 28 秒涨到 29 秒,几乎无感。运行时多出的开销在 embedding 提取和聚类上,对 30 分钟级别的音频,体感上慢一点,但不会卡住。真正的坑在内存:cam++ 跑长音频会把整段 wav 一次性读进内存做聚类,我机器 16GB,1 小时以上的录音有概率 OOM,这个我提前记下了。funasr 社区给的折中是手动按 60 秒切片,逐段推理再合并 segments,代价是跨段同一个人可能被编号两次,下面会讲怎么用一段后处理统一编号。

5.2 同一段录音,再跑一次

把上一节 34 分钟的双轨立体声录音原样丢回升级后的服务,response_format 还是 verbose_json。这次我特意挑了机器空闲的时间重测,跟第一次压测的条件对齐,数字放一起对比着看:

升级前 升级后
HTTP 状态 200 200
端到端耗时 1159 秒 587.1 秒
RTF 0.56 0.28
segments 数 8801(字级) 273(句子级)
响应体大小 379KB 111KB
顶层说话人字段 spk_enabled / speakers / speaker_count / speaker_text
segment 字段 start / end / text start / end / text / speaker

两组数字放一起有个反直觉的点:加了说话人分离,总耗时反而降了一半多。我拆开看原因:旧版本返回的是字级 timestamp,8801 段全是空 text,文本只在顶层 text 字段拼了一次,后处理得遍历 8801 条;新版本开了 spk 之后,funasr 同时返回 sentence_info,句子级切分天然把 segments 收成 273 段,拼接、序列化的开销跟着降下来了。所以要不要上 spk,不能只看 RTF 涨没涨,得看完整的端到端数字。

5.3 JSON 怎么体现说话人

升级后的 verbose_json 多出三类字段,纪要类应用最关心的就是 speaker_text,一行一段会议发言:

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| {`` "spk_enabled": true, "speakers": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], "speaker_count": 11, "text": "...", "speaker_text": "[00:13-00:18] 发言人1: 年会的事情办几天比较合适都,\n[00:19-00:20] 发言人2: 是一天吧就安,\n[00:23-00:27] 发言人1: 排在工作室工作日周五周,\n[01:18-01:42] 发言人3: 如果这个你还是得看预算......", "segments": [ {"start": 13.27, "end": 18.08, "text": "年会的事情办几天比较合适都,", "speaker": 0}, {"start": 19.14, "end": 20.125, "text": "是一天吧就安,", "speaker": 1}, {"start": 23.81, "end": 27.96, "text": "排在工作室工作日周五周,", "speaker": 0} ] } |

speaker_text 是把 segments 按时间排好、相邻同说话人合并、再把时间戳格式化成 [mm:ss-mm:ss] 之后拼出来的,直接复制到飞书/钉钉就能当会议纪要草稿。字段顺序我按"常用程度"排的:纯文本拿 text,要带时间戳和角色拿 speaker_text,要细到每个句子做二次编辑拿 segments,前端按需取用就行。

5.4 11 个说话人是怎么排出来的

这次录音识别出 11 个 speaker,分布很悬殊(段数是合并前的统计):

说话人 累计发言时长 段数
发言人1 863 秒 92
发言人2 592 秒 94
发言人3 284 秒 43
发言人4 119 秒 29
发言人5~11 4~22 秒 各 1~6 段

这种长尾分布符合我对这种多人会议的预期:三四个人主导会议,剩下的人偶尔插话。cam++ 是无监督聚类,不预设人数,靠声纹相似度自动归并,所以"是不是同一个人"完全靠模型判断。实际用下来,置信度高的判定集中在前 3、4 个主要说话人上,后面那些只有几秒钟发言的角色,有一定概率是误把同一个人在不同情绪下的声音切成了两段,短发言的编号可信度要打个问号。真要再提一档准确率,社区有两条路:一是上 speaker-verification 做二次校验,二是换更大的说话人模型,代价是推理变慢。我现阶段把这事往后排,等真有"分错人会被投诉"的场景再说。

5.5 跨段编号一致性的坑

切 60 秒段推理会带来一个隐藏问题:每段内部的 speaker 编号是局部的 0/1/2,跨段可能第 0 段和第 1 段其实是同一个人。不做归一化,speaker_text 里就会出现"发言人1 → 发言人2 → 发言人1"的反复横跳。

修法是在拿到全部 segments 之后做两件事:第一,把所有出现过的局部 spk 按出现顺序映射成全局连续编号 0,1,2,...,N-1,前端不用再处理编号冲突;第二,把相邻的、speaker 相同的 segments 合并成一段,让 speaker_text 读起来更像"一个人在说完一整段话",而不是按 VAD 切碎的一句一句。代价是段数从 273 段进一步收窄到 200 段上下,时间戳按最早的开始和最晚的结束算,对会议纪要这种"一个人说一段"的呈现方式刚好。

5.6 几条工程经验

**MODELSCOPE_CACHE 提前指好。**cam++ 首次启动会从 ModelScope 拉权重,把环境变量指到一个已经预热的目录(我机器上两套 ASR 服务共用同一个 models/ 目录),启动秒级完成,不用等下载。生产环境做镜像的时候把这步挪到 Dockerfile 的 RUN 阶段固化下来,不要让 pod 启动时去拉。

**spk 段切多大需要试。**官方没给硬性建议,我试了 30 秒、60 秒、120 秒三档:30 秒切得太碎,跨段归一化压力最大;120 秒单段推理慢但编号稳定。60 秒是性价比拐点,30 分钟级音频切成 30 段,每段约 17 秒处理,总耗时跟 5.2 节那张表一致。再长的音频把切片长度调大,我估计 2 小时以上的录音得提到 120 秒甚至更长,或者干脆走流式方案。

speaker 字段别给前端埋雷。 null 一定要明确写出来,不要省略字段。我处理的时候,所有没识别到 spk 的 segment 都写成 "speaker": null,而不是直接不写这个 key。这样前端判空是 s.speaker === null 单一逻辑,不用兼容"字段不存在"和"字段是 null"两种情况。

最后说性能。开了 spk 之后,11 个说话人、34 分钟音频的完整 pipeline(VAD 切段、paraformer 推理、ct-punc 标点、cam++ 聚类、segments 合并)在 CPU 上跑 587 秒,比没开 spk 还快一点。这倒不是说 cam++ 不算力,而是 sentence_info 段落比字级 timestamp 少一个量级,后处理总时间被压下去了。真正要抠的是切片边界处的 ctx 重叠,这版先不展开,下一篇专门写"30 分钟以上长音频怎么打"。

六、上线前要补的事

官方 openai_api 示例的部署页里写得很直白:示例服务不内置生产认证、租户配额和完整可观测性,这些得靠网关和基础设施补齐。我按这个思路列了清单:

**TLS 和鉴权。**网关层终结 TLS,接口做 API Key 校验,别把 8000 端口裸奔到公网。

**限流。**IP 维度加 QPS 限制,防止被脚本刷爆,Nginx 的 limit_req 模块就够。

**上传限制。**大小、MIME、请求时长三重限制,服务端做兜底,别只靠网关一层。

**进程守护。**nohup 不行,macOS 用 launchd 配 KeepAlive,进程死了自动拉起。这个我踩过,不配好隔几天就得手动救一次。

**监控。**RTF 的 P50/P95、错误率、队列深度,这三项进告警,够用就好。我的习惯是先看趋势再看突发,别一上来就上全链路监控。

版本固定和冒烟。 权重、依赖版本锁死,官方建议发布前固定依赖并保留回滚;每次升级先跑官方仓库的 smoke_test.py 再放量,这套流程成本很低,值得养成习惯。

热词和说话人。 hotword 参数业务侧按需传,cam++ 加一行代码的事,这两个能力是自建最大的红利,云 API 要么额外收费,要么干脆不给。

另外提醒一个内网部署的坑:FunASR 的 ModelScope SDK 启动时会做联网校验,即使模型已经缓存过,纯内网环境也可能卡在这一步,社区 issue #2573 有记录。我预估内网环境大概率会踩这个,到时候要么提前预热并放行校验域名,要么直接走官方 llama.cpp/GGUF 路线------那是标了"生产验证"的方案,单二进制、内置 VAD、不用 Python 运行时,q8 量化后体积减半,边缘设备也能跑。

还有一个容易忽略的点:~/.cache/modelscope 这个权重目录建议纳入备份。重装机器、换电脑,最痛的不是重装 Python,是重新下 2GB 权重。

最后一个兜底建议:本地服务挂了,网关自动切云 API 降级,别让转写链路断在单点上。降级在网关层做,/v1/audio/transcriptions 失败时自动转发到云 API,客户端无感。自建是常态路径,云是保险丝。这个我打算上线第一周就配上,宁可多花点云 API 的钱,也不能让业务断。

写在最后

自建语音转写,门槛不高,但要做得稳,坑都在细节里。我实测下来的结论:环境坑比模型坑多,官方标了"生产验证"的路径可以放心走,但认证、限流、监控这三件事,官方明确留给你自己补,别指望示例代码替你解决。再说句心里话:自建最值钱的不是省下的服务费,是我想要什么能力都能自己加,不用求人。

转写量大、音频敏感的朋友,值得自己搭一套;偶尔用几回的,云 API 更省心。觉得有用的话,顺手点个关注,后面我会继续写本地 AI 服务落地的实测。


【欢迎访问我的个人博客主页,这里有我的精选文章和AI大模型日报专栏。👇)

本文用到

1: FunASR OpenAI 兼容私有 API 部署页. OpenAI 兼容私有 API - 工业部署 - FunASR

2: FunASR openai_api 示例. https://github.com/modelscope/FunASR/blob/main/examples/openai_api/README.md

3: FunASR 官方文档. FunASR - End-to-End Speech Recognition Toolkit

4: FunASR 说话人分离 (cam++) 文档. CAM++说话人确认-中文-通用-200k-Spkrs

5: AutoModel spk_model 参数说明. https://github.com/modelscope/FunASR/blob/main/docs/installation.md

相关推荐
没落之王1 小时前
易学使者郑氏正脉郑冰推演马航事件
大数据·人工智能·算法·机器学习·可用性测试
wy_hhxx1 小时前
AI Agent & Harness Engineering 笔记
人工智能·笔记
yuhulkjv3351 小时前
Grok鸿蒙版导出word格式的终极解法:AI导出鸭如何重构AI内容到文档的最后一公里
人工智能·ai·word·harmonyos·ai导出鸭
ONEYAC唯样1 小时前
2026年中国电子元器件进出口全景分析:AI时代下的供应链重构与产业机会
人工智能
aneasystone本尊1 小时前
学习 Headroom 的输出 token 优化
人工智能
武子康1 小时前
给 Pi 增加能力时,应该写 Prompt、Skill、Tool 还是 Extension?
人工智能·llm·agent
love530love1 小时前
彻底清理 Windows 右键“打开方式“中的重复/失效程序项(PyCharm 多版本残留实战 + 自动化脚本)
运维·人工智能·windows·pycharm·jetbrains·toolbox
必须会一定会1 小时前
Node.js Agent Handoff 仓库扫描 MVP:忽略规则、include 通配与稳定输出实现
人工智能·node.js·ai编程
Fluxart.ai1 小时前
Etsy手工制品换背景,用什么AI能保留手作质感?
前端·javascript·人工智能