
如果你跟我一样被一堆企业培训录音、客服通话质检、会议纪要给追着跑大概率会明白“有一个能离线跑的语音识别服务”有多香。上个星期我基于 FunASR 把语音识别转写服务部署到了生产环境实测 511MB 的音频文件转写只花了 47 秒。这个结果算不上顶级但足够让老板闭嘴。这篇文章我打算把 FunASR 从环境搭建、模型下载、服务封装到性能压测的整个流程拆成 8 步同时把我在上线过程中踩过的 7 个坑一次性交代清楚给正在准备搞离线语音识别的朋友一个完整参照。我把这个项目定位成“生产部署实战”不是演示 demo也不是模型论文复现。部署的目标很简单在纯内网环境里提供稳定的语音转写 HTTP 服务支持长音频切片、热词干预、标点恢复并且能扛住一定并发。过程中会用到阿里开源的 FunASR 工具包以及配套的 Paraformer、FSMN-VAD、CT-Transformer 标点模型。如果你也是第一次接触这套东西跟着我的步骤走应该能少走不少弯路。1. 项目拆解511MB音频47秒背后的设计思路1.1 为什么是 FunASR 而不是云端 API 或其它开源框架说实话市面上能转写的方案不少。云端 API 识别质量高但数据要出网光合规评审就能拖两周而且按分钟计费生产环境天天跑成本很高。其它开源语音识别工具我也试过有的部署简单但准确率一般准确率不错的又对长音频支持不友好。最后还是选了 FunASR。FunASR 是阿里开源的中文语音识别工具包底层模型是 Paraformer在中文识别上非常能打。它把语音活动检测VAD、语音识别、标点恢复都串在了一个 Pipeline 里直接把“长音频文件”丢进去就能输出带标点的完整文本。这点对生产环境来说太关键了省掉了很多自己写状态机的麻烦。而且它支持 PyTorch 环境模型可以方便地私有化落地符合内网部署的要求。1.2 离线落地的整体架构三个模型一个服务我设计的架构不复杂但足够生产用。核心是三个模型FSMN-VAD 负责把长音频切成有人声的片段Paraformer-large 负责把每个片段识别成文字CT-Transformer 负责把文字加上标点。三个模型都加载在同一个进程里通过 FastAPI 封装成一个 HTTP 服务。这样做的好处是模型常驻内存不像某些方案每次请求临时加载。音频上传进来后服务先调 FFmpeg 归一化成 16kHz 单声道 PCM再走 VAD 切段然后逐段识别最后做标点拼接。整个链路在进程内完成延迟可控也方便加批处理。1.3 那个 511MB 和 47 秒到底是怎么来的这次测试的音频是一段企业培训录像提取出的双声道 MP3总时长 3 小时 45 分钟文件体积 511MB。前期我把音频解压成 16kHz 单声道 PCM长度大概是 1.2GB通过 FSMN-VAD 切成了两千多段有效语音。识别环境是一块 A10 显卡模型用 FP16 半精度推理batch_size 调到 16最后跑完全部音频用了 47 秒。算下来实时率大概是 0.0035 左右。我看很多文章说“实时率 0.1 就很不错了”这里之所以能这么快关键在于 VAD 把静音段大量跳过了真正送去模型的语音段可能只有总共 1.5 小时而且 GPU 批量推理用得很充分。所以这个小标题不是噱头而是架构优化之后的真实结果。2. 部署前必做的模型选型与参数规划2.1 模型挑对了后面少折腾一倍FunASR 的模型仓库里东西不少别想着全装按需求挑就行。语音识别主模型我选的是paraformer-large它是当前中文识别精度最高的一档。如果你对识别速度更敏感且音频是标准普通话可以选paraformer或者paraformer-zh的轻量版本模型体积更小推理更快。VAD 模型选的是fsmn-vad专门用来检测语音起始点。长音频必须配它不然一口气把几小时音频送进识别模型显存直接爆掉。标点模型选的是ct-punc负责恢复标点符号。如果你后面还要做重点词频统计、情绪分析标点模型反而是刚需。下面这张表是我这次用的模型组合可以直接参考用途模型名称说明语音识别paraformer-large中文识别精度高适合正式场景标点恢复ct-punc输出带标点的文本VADfsmn-vad长音频切段降低显存压力2.2 参数规划batch_size、FP16、并发怎么取舍模型选完之后最影响上线效果的是参数策略。先说要决策的几个关键项batch_size、fp16、并发处理方式。语音识别虽然看起来像文本任务但实际上是计算密集型的音频任务batch_size 设得太小GPU 跑不满设得太大显存瞬间告急。我整理了一个经验公式供参考显存占用约等于batch_size × 最大音频帧数 × 模型特征通道数 × 字节数。Practically如果你用 A1024GB 显存Paraformer-large 模型用 FP16VAD 切出的音频段平均 6 到 10 秒batch_size 开到 16 是安全的如果音频段全是 10 秒以上建议降到 8。另外FP16 基本是必选项显存占用直接砍半推理速度还能往上提A10 和 V100 都支持得很好。并发层面我建议一个进程里加载模型然后用线程池处理请求。对单个 GPU 来说并发太高没意义反而会触发显存竞争。真正调并发的时候观察 RTF 和 P95 延迟比盯着 QPS 更靠谱。2.3 一份能用的生产级配置示例我习惯把模型路径、批处理大小、半精度开关都收敛到一个 JSON 配置文件里方便统一管理。下面这个是生产环境的配置骨架{ asr_model: /opt/models/paraformer-large, vad_model: /opt/models/fsmn-vad, punc_model: /opt/models/ct-punc, mode: offline, batch_size: 16, max_single_segment_time: 20000, half_precision: true, device: cuda:0, hotword_path: /opt/models/hotword.txt }max_single_segment_time这个参数我建议锁在 20000 毫秒防止 VAD 切出来的某一句话特别长batch 内其它音频都要等它跑完。生产环境里“稳定”优先于“单条刷分”这个原则贯穿始终。3. 离线部署8步走从环境到压测的完整实录3.1 第1步准备干净的 Python 与 GPU 环境FunASR 基于 PyTorch环境乱了后面大概率起不来。我用的是 Ubuntu 20.04NVIDIA 驱动版本在 470 以上CUDA 11.8。Python 版本建议锁 3.8 或 3.10别用 3.12部分依赖还没对齐。用 conda 建环境最省心conda create -n funasr python3.10 -y conda activate funasr然后单独装对应 CUDA 的 PyTorch别让 pip 自动装 CPU 版本。我这边用的是官方命令安装的 PyTorch 2.0.1cu118。装完立刻检查一下 GPU 是否可见python -c import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))这一步能筛掉一半环境问题。如果输出False或者报错先回头检查驱动和 PyTorch 版本匹配不要硬往后走。3.2 第2步安装 FunASR 与核心依赖环境干净后安装 FunASR 和模型下载工具。直接用 pip 安装pip install funasr modelscope torchaudiomodelscope用来下载模型torchaudio主要做音频解析。这里强调一下不要一股脑装最新版。在写这篇文章时我用的组合是funasr1.1.3搭配modelscope1.14.0实测稳定。如果你跟着官方 main 分支跑可能会遇到接口变化比如AutoModel的初始化参数名不一样建议锁版本上生产。3.3 第3步下载离线模型并检查完整性模型一般要从 ModelScope 拉。你可以先用下面这个脚本在一台能联网的机器上下载全部模型再拷贝到内网from modelscope.hub.snapshot_download import snapshot_download models [ iic/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-pytorch, iic/speech_fsmn_vad_zh-cn-16k-common-pytorch, iic/punc_ct-transformer_zh-cn-common-vocab272727-pytorch, ] for model in models: snapshot_download(model, cache_dir/opt/models_download/)下载完成后检查模型目录里的config.yaml、am.mvn、model.pt是否存在。常见坑是文件下载不完整导致加载时直接报unexpected EOF。可以用ls -lh看文件大小是否和远端标注一致也建议记录下sha256离线搬运之前校验一次。3.4 第4步离线搬运与模型目录私有化到了内网服务器上我的习惯是把模型统一放到/opt/models下不散落在代码仓库里。这样做有几个好处模型文件量大和代码分开好做备份后续升级模型只替换目录即可不用动服务代码。搬运时用 tar 打包更稳妥cd /opt/models_download tar czf models.tar.gz iic到内网服务器解压后再把关键目录重命名成不含版本号的标准路径例如mv /opt/models/iic/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-pytorch /opt/models/paraformer-large。VAD 和标点目录同理。配置完成后必须把目录设置成只读防止进程运行时误写模型目录。大部分线上事故都是权限太随意造成的chown -R root:root /opt/models chmod -R a-w /opt/models3.5 第5步编写服务启动入口核心服务我用 FastAPI 封装。模型在进程启动时加载一次之后常驻内存。下面是一个能直接用的骨架from fastapi import FastAPI, UploadFile from funasr import AutoModel import numpy as np import io import soundfile as sf app FastAPI() model AutoModel( model/opt/models/paraformer-large, vad_model/opt/models/fsmn-vad, punc_model/opt/models/ct-punc, devicecuda:0, fp16True, batch_size16, ) app.post(/api/transcribe) async def transcribe(file: UploadFile): data, sr sf.read(io.BytesIO(await file.read()), dtypeint16) if sr ! 16000: # 这里需要先转采样率实际建议用 ffmpeg 预处理 pass res model.generate(inputdata) text res[0][text] return {text: text}注意这里我用soundfile读取音频但生产环境里用户上传的文件格式五花八门MP3、AMR、WAV 都有。所以实际实现建议在服务入口先通过 FFmpeg 统一转成 16kHz 单声道 PCM再交给 FunASR避免格式问题把模型搞崩。3.6 第6步启动前自检服务写好后先别急着接流量跑一遍自检。检查项包括模型是否能全部加载到 GPU 上VAD 是否能识别静音简单音频转写是否为中文乱码。我通常准备一个 10 秒左右的纯普通话 WAV 文件用curl直接测试curl -X POST http://127.0.0.1:8000/api/transcribe \ -F filetest.wav如果返回的中文正常再用一段带静音的长音频测试 VAD 切段是否生效。这个阶段最容易发现模型路径写错、音频采样率不对、标点模型缺失之类的问题。自检脚本我建议固化到deploy/selfcheck.sh里后面迭代版本可以直接跑。3.7 第7步接口联调与批量转写服务自检通过后就开始做批量转写。如果只是几十个文件循环请求接口就行for f in audio/*.wav; do curl -s -X POST http://127.0.0.1:8000/api/transcribe \ -F file$f -o result/$(basename $f).json done但生产环境往往有几千个文件我建议写个 Python 客户端用ThreadPoolExecutor控制并发比如 4 个并发跑批量任务。这样既能压测服务稳定性又能提前暴露内存泄漏问题。我在这个阶段跑完了那个 511MB 的音频输出的文本存成 JSON 和纯文本两份方便下游做 NER、关键词抽取。3.8 第8步压测与监控收敛参数最后一步是压测。语音服务压测和普通 HTTP 接口不一样重点看两块GPU 显存峰值和 RTF。我写过一个简单脚本用 20 个并发线程持续发送 30 秒音频观察服务会不会崩、响应时间是否线性增长。我用nvidia-smi监控显存同时记录每个请求的响应时间。如果显存居高不下并且请求开始排队说明 batch_size 开大了如果 GPU 利用率不到 30%说明batch_size太小或者 I/O 卡在音频解码上。真正的生产参数是在这一步收敛的我最终把batch_size稳定在 16并发线程数稳定在 4效果是 511MB 音频 47 秒跑完。4. 踩坑记录7个生产环境的典型问题4.1 坑1CUDA、PyTorch 版本错位导致显存非法访问部署第一天就遇到CUDA error: invalid device function。排查下来是 PyTorch 装成了 CPU 版本但代码里又指定了cuda:0。后来卸载重装 cu118 版 PyTorch 解决。我的建议非常简单安装完 PyTorch 后立刻跑一次 GPU 验证不要装完就继续。很多时候版本错位不是显性问题等模型加载到一半才炸排错成本更高。4.2 坑2ModelScope 下载中断模型文件缺斤少两下载paraformer-large的时候网络抖动导致模型只下了一半。FunASR 加载时不会立刻报错而是到真正推理时吐乱码。这类问题最阴间。后面我学乖了下载脚本里加了文件大小校验并且下载完成后检查目录内模型文件数量。强烈建议你把模型下载步骤自动化别靠人肉盯着。4.3 坑3热词配置不生效专业名词识别错得离谱生产环境里客户要求把“妙鸭相机”这类品牌词识别正确但默认模型总是识别成“喵鸭相机”。我一开始在请求里传了hotword参数结果没生效。后来翻了 FunASR 源码才发现离线模式下热词有两种方式一种是模型目录下的hotword.txt另一种是调用generate时传hotword参数。我最后把热词写入hotword.txt才稳定生效。注意热词文件格式是每行一个词模型会自动加权。不要塞太长的短语4 到 8 个字效果最好。4.4 坑4长音频不做 VAD 直接 OOM进程被杀刚开始测试的时候我图省事直接调用识别模型处理完整音频结果音频超过半小时后 GPU 显存直接爆掉服务进程被系统杀掉。后来把vad_model参数传进去才把长音频拆成一段段处理。这里特别提醒即使有 VAD也要设置max_single_segment_time不然 VAD 会把连续人声切成一个超长段照样 OOM。4.5 坑5模型加载太慢第一包延迟高到无法接受服务重启后第一个请求要等 30 秒以上因为模型在第一次推理时才懒加载。对于生产环境这严重不可接受。解决方案很简单应用启动后在健康检查接口里发一个静音音频做 warmup让模型先完成加载和编译。我写了一个/health接口里面除了返回 200还会触发一次空转推理确保模型真正热起来。4.6 坑6VAD 切段太碎标点模型把句子断得支离破碎VAD 调节太灵敏时会把一句话切成好几段标点模型对每段单独加标点导致输出很多句号读起来像电报体。后来我把 VAD 的min_silence_duration调大了一点并且允许 0.8 秒以内的静音保持同一段断句问题明显缓解。4.7 坑7内网离线环境 pip install 卡死部署进度归零到了纯内网环境直接pip install funasr肯定会失败。我提前在联网机器上把依赖全拉下来了pip download -r requirements.txt -d /packages --platform manylinux2014_x86_64 --python-version 310 --only-binary:all:然后在服务器上pip install --no-index --find-links/packages -r requirements.txt。这样内网部署就不会被网络卡住。如果公司有自己的pip index更好。下面是 7 个坑的速查表方便你直接抄坑编号现象根因解决方案1CUDA invalid device functionPyTorch 版本不匹配装匹配 CUDA 的 PyTorch安装后立即验证 GPU2推理乱码模型文件下载不完整下载脚本加校验搬运前检查文件大小3热词不生效hotword 参数没用对使用模型目录下的 hotword.txt4GPU OOM长音频未切段启用 VAD限制单段最大时长5首包延迟高模型懒加载warmup 推理常驻模型6标点破碎VAD 切段太碎调大 min_silence_duration7内网依赖装不上没有离线包提前 pip download 内网安装上面这 7 个坑每一个都让实际部署进度至少卡了半天。尤其是热词和内网依赖属于“文档里很少写清楚踩一次终生难忘”的典型问题。如果你也正准备把 FunASR 拉上线我建议你提前把离线依赖包和模型校验脚本准备好真到了内网环境你会感谢现在的自己。