ASR 折腾记:从 Whisper 到多模型 Pipeline 的半年探索

本文由 Claude Code (DeepSeek v4) 代写,suyiiyii 审阅。

摘要

在 RTX 2060 SUPER 上,从搭一个 Whisper HTTP 服务开始,到折腾 7 个 ASR 模型、改异步队列、修 WDDM TDR、拼 speaker diarization、写 benchmark——半年时间,把一台 Windows 游戏机搞成了能打的生产级语音转写平台。

背景

有录音转写的需求。一开始的想法很简单:pc-5950x 上有张 RTX 2060 SUPER 8G,闲着也是闲着,装个 Speaches(faster-whisper 的 OpenAI 兼容包装),起个 HTTP API,完事。

2026 年 2 月 21 号,照着文档把 Speaches 部署好了。Whisper large-v3,GPU float16 推理,POST /v1/audio/transcriptions,返回 {"text": "..."}。挺顺利的。

然后发现 Whisper 的中文转写质量不太行。行吧,再部署一个 Qwen3-ASR-1.7B。2 月 27 号搞完,端口 8002,同样是 OpenAI 兼容 API。两个服务并存,客户端切个 base_url 就能换模型。

一切都很好。直到服务开始莫名其妙挂掉。

Windows 教你做人——WDDM TDR

服务又挂了

某天打开 Uptime Kuma,8002 端口红了。

1
2
$ curl http://100.67.187.87:8002/health
curl: (7) Failed to connect

SSH 上去看,GPU 正常、系统没重启、端口没监听。去看日志文件——全部 0 字节。进程被杀了,但没留下任何痕迹。

这种事情发生了不止一次。4 月 12 号一次,4 月 26 号又一次。每次都是手动重启,过几天又挂。

根因

经过深入调查(读了半天微软文档),找到了罪魁祸首:WDDM TDR(Timeout Detection and Recovery)

Windows 有一个 GPU 保护机制:如果 GPU 操作超过 2 秒没响应,系统会直接杀掉进程并重置 GPU 驱动。不打招呼,不给时间写日志,秒退。

RTX 2060 SUPER 是消费级显卡,不支持持久化模式。而且通过计划任务启动的后台进程,更容易被系统资源管理器"特殊照顾"。

Qwen3-ASR 跑的是自回归推理,一个长 chunk 的 GPU 操作可能超过 2 秒——然后就被 Windows 判了死刑。

一开始以为是 Python 代码的问题——可能是显存泄漏、可能是 uvicorn 挂了。看了半天日志(空的),又检查了 GPU 状态(正常),最后查到 Windows 事件查看器才发现:进程是被系统杀的。根因是 Windows 的设计限制,不是代码 bug。

解决方案

三步走:

1. 改 TDR 超时

1
2
reg add 'HKLM\SYSTEM\CurrentControlSet\Control\GraphicsDrivers' /v TdrDelay /t REG_DWORD /d 10 /f
reg add 'HKLM\SYSTEM\CurrentControlSet\Control\GraphicsDrivers' /v TdrDdiDelay /t REG_DWORD /d 10 /f

从 2 秒改到 10 秒。不是根治,但大幅降低了触发概率。

2. 自动监控 + 重启

写了个 PowerShell 脚本,每 5 分钟调一次 /health。挂了就自动重启。最多 5 分钟 downtime,不需要人工干预。

3. 提升进程优先级

启动脚本里加了 PriorityClass = "AboveNormal"

搞完之后:可用性从 ~95% 提到了 >99.5%。虽然 WDDM TDR 的根本问题还在(这是 Windows 的事),但至少不会一挂就是半天没人管了。

转写质量——“14 分钟音频只出了 1180 个字?”

并发崩溃 + 文本截断

5 月 5 号,遇到了两个新问题。

第一个:3 个 subagent 同时提交 14 分钟音频到 /v1/audio/transcriptions,服务直接崩了。原因是同步 API 设计——客户端上传文件后要维持长连接等 4 分钟处理完。3 个大文件同时加载到内存,OOM。

第二个更严重:14 分钟的音频,转写出来只有 ~1180 个字符。同样的音频,火山引擎(豆包)付费 API 给出了 5606 字符。差了将近 5 倍。

排查

先怀疑 max_new_tokens=256 不够。改成 2048——结果还是 1180 字。不是 token 限制。

又怀疑是分块的问题。尝试去掉分块,840s 音频整段直传模型——encoder 直接 OOM。8G 显存确实装不下。

emmmmm。

回看之前的边界测试数据:30s chunk → 6.0 chars/s,300s chunk → 1.35 chars/s。

💡!chunk 越大,模型越倾向于"总结式"转写而非"逐字式"转写。300s 的 chunk,模型觉得"这太长了,我帮你概括一下吧"——然后就只输出了 20% 的内容。

解决

异步 Job 队列:把同步 API 改成异步。POST 立即返回 job_id,后台 Worker 串行处理,客户端轮询。不再崩了。

分块优化MAX_CHUNK_MS 从 300000 → 120000,max_new_tokens 从 256 → 2048。

结果:

方案 chunk max_tokens 字符数 完整度
—— :—: :—: :—: :—:
v0.8.0 本地 300s 256 1,180 ~20%
v0.9.0 本地 120s 2048 5,620 ~95%
火山引擎付费 - - 5,606 ~95%

本地 ASR 和付费 API 基本持平了。虽然速度慢一些(RTF ~1.0x,就是 1:1),但质量能打。😁

模型大乱斗——到底哪个最好用?

Qwen3-ASR 能用,但太慢了。14 分钟音频要跑 14 分钟。于是开始物色替代方案。

候选模型

拉出来测了 6 个模型:

模型 类型 参数量 架构
—— —— :—: ——
Whisper large-v3 纯声学 1.5B Transformer
Qwen3-ASR-1.7B LLM 骨架 1.7B 自回归
SenseVoice-Small 纯声学 234M 非自回归
Paraformer-large 纯声学 220M 非自回归
Fun-ASR-Nano-2512 LLM 骨架 800M LLM+encoder
GLM-ASR-Nano LLM 骨架 1.5B LLM+encoder

测试音频统一用了一段 54 秒中文录音 + 3 分钟片段 + 11.7 分钟完整音频。

结果

纯声学模型(SenseVoice、Paraformer)

快是真的快。SenseVoice-Small 的 RTF 只有 0.01x——14 分钟音频 8 秒搞定,100 倍于 Qwen3-ASR。显存才吃 1.5G。

但专有名词全崩:

  • chatgpt.comchGBT 点 com
  • chatgpt.comcharge BT 点 com

音近误识。纯声学模型没有语言模型纠错,遇到不常见的词就直接音译。

带 LLM 骨架的模型

Qwen3-ASR 短音频的专有名词最好(chatgpt.com 正确识别),但长音频会退化——字/秒从 3.5 跌到 1.2,后半段基本在划水。

GLM-ASR-Nano 更离谱——长音频直接崩溃/编造。

Fun-ASR-Nano:唯一一个长音频不退化、专有名词准确的。11.7 分钟全程 3.5 字/秒稳定输出,CHATGPT.COM 正确识别。

但是。Fun-ASR-Nano 的 VAD 模块有 bug。funasr 1.3.1 版本,vad_model="fsmn-vad" 直接抛 KeyError: 0。修了半天——去翻了源码,发现是 VAD 后处理时一个字典 key 不存在,加了个 .get() 搞定。

选定

Fun-ASR-Nano 胜出,部署为生产服务。速度确实比 SenseVoice 慢(98s vs 8s),但质量靠谱。

指标 Qwen3-ASR (迁移前) Fun-ASR-Nano (迁移后)
—— :—: :—:
11.7min 耗时 165.9s 98.4s
11.7min 文字数 841 字 2477 字
长音频退化 ⚠️ 字/秒从 3.5→1.2 无退化
chatgpt.com chatgpt.com CHATGPT.COM
参数量 1.7B 800M
显存 ~3.5GB ~4GB

能用就行,又不是不能用。😝

顺便:暴露到公网 + LLM 后处理实验

既然搞好了,那就暴露出去用。通过 99.suyiiyii.top 的 Caddy 反代 + Tailscale 内网穿透,把 8002 端口暴露到了公网:

1
2
公网  https://asr.99.suyiiyii.top:443 (Caddy TLS+Basic Auth)
      Tailscale  100.67.187.87:8002 (pc-5950x WSL2)

加了 Basic Auth,没有认证返回 401。Let’s Encrypt 证书 Caddy 自动续,不用管。

闲着也是闲着,还试了下用 DeepSeek 给 ASR 结果做后处理。确实能修一些同音错词——淘定律韬定律——但也可能把原文"合理化改写"。最后决定:后处理默认关,保留原文。语义真实性优先。

上云——StepAudio 2.5 + Speaker Diarization

为什么又换?

Fun-ASR-Nano 跑了三周,发现了两个问题:

  1. 长音频时间戳覆盖异常——超过 30 分钟的音频,时间戳就开始漂移
  2. 说话人识别不行——多人对话场景,speaker 标签基本不可用

算了笔账:StepAudio 2.5 的 API 费用 ~0.15 元/小时。按每个月转写 20 小时算,一个月 3 块钱。低于心理预算(0.25 元/小时)。

“又不是不能用” → “那就用更好的”。💡

ASR 8006 架构

5 月 31 号,部署了新的 8006 服务:

  • 主转录:StepAudio 2.5 ASR(云端),不做 LLM 改写,不做 ITN
  • Speaker:本地 CAM++ + pyannote ensemble,gate 通过才输出,不稳定就 suppress
  • 设计原则:语义真实性 > 经济性 > 说话人识别 > 效率 > 语气保真

为了客观评估,设计了 5 段 benchmark:

样本 时长 内容
bv1zr_chip_full 11.3min 差评君:为什么汽车需要一块不一样的芯片
bv1iv_luofuli_first30m 30min 罗福莉 3.5 小时访谈
bv1yr_yaoshunyu_first60m 60min 姚顺宇 4 小时访谈
tuanshanbeilu_full 27.9min 团山北路(多人对话)
pause_lab_full 16.7min 暂停实验室播客

以豆包 ASR 输出作为近似基准,用文本相似度做客观对比。

Benchmark 结果

样本 文本相似度 (norm) 速度 Speaker
—— :—: :—: ——
bv1zr_chip_full 0.9696 9.10x stable / 1 人
bv1iv_luofuli_first30m 0.9260 7.32x stable / 2 人
bv1yr_yaoshunyu_first60m 0.9500 7.47x stable / 2 人
tuanshanbeilu_full 0.8999 9.99x stable / 3 人
pause_lab_full 0.9856 8.36x stable / 1 人

5/5 speaker gate 通过,全部选了 CAM++。速度 7-10x,远超 2x 目标。文本相似度 0.90-0.99 vs 豆包。

团山北路的相似度最低(0.8999),因为那是多人自然对话,三家 ASR(豆包、StepAudio、Fun-ASR)的输出风格差异很大——豆包偏书面整理,StepAudio 偏口语保留。不是谁对谁错的问题,是风格不同。

顺便一提:团山北路的 speaker,豆包给了 8 个人——但实际只有 3 个人在说话。这也是为什么 speaker 不能直接用 ASR 自带的结果,要自己做 diarization。

当前架构总览

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
┌──────────┐     ┌──────────────────┐     ┌─────────────────────┐
 客户端    │────▶│ Caddy (99 ECS)   │────▶│ WSL2 (pc-5950x)     
                asr.99.suyiiyii.                           
                top:443                8006 StepAudio 主路  
                Basic Auth + TLS       8002 Fun-ASR (backup)
└──────────┘     └──────────────────┘     └─────────────────────┘
                                                   
                                    ┌──────────────┴──────────────┐
                                     StepAudio 2.5 ASR (云端)     
                                      16k mono 规范化           
                                      30min chunk               
                                      不做 ITN                  
                                      费用 ~0.15 /小时        
                                    ├─────────────────────────────┤
                                     Speaker Diarization (本地)   
                                      CAM++ + pyannote ensemble 
                                      speaker gate              
                                      可选 anonymous memory     
                                    └─────────────────────────────┘

总结

半年,一台 RTX 2060 SUPER,7 个模型,无数次崩溃和重启。

从"搭个 HTTP 服务就完事"到"多模型 pipeline + speaker diarization + benchmark 体系",中间踩的坑比想象的多得多。

Windows 跑 GPU 服务是真的折磨——WDDM TDR 一生之敌。但也确实能跑,改改注册表、加个监控,又不是不能用。

模型方面:纯声学模型快但专有名词差、LLM 骨架模型质量好但慢且长音频不稳定——目前没有一个"全都要"的完美方案。Fun-ASR-Nano 是本地方案里最好的,但离商用 API 还有差距。StepAudio 2.5 云服务性价比不错,0.15 元/小时,一杯奶茶钱转写几十个小时。

Speaker diarization 这条线才刚开始。CAM++ ensemble + gate 的框架搭好了,benchmark 也过了,但团山北路的 speaker 时间轴还没做人工复核。下一步要把 speaker memory 从匿名 cluster label 升级到真实声纹 embedding。

总的来说还是一次很爽的折腾。从遇到问题到不断换方案,每一步都学到了东西。现在这套东西跑得挺稳的,以后有新模型出来再接着测。💪


相关阅读:[我的 homelab(5): nerdctl:docker 的升级版 / 镜像拉取缓存和加速](https://www.notion.so/p/我的-homelab5-nerdctldocker-的升级版 - 镜像拉取缓存和加速/)[k8s 折腾记:使用 Loki 统一管理集群日志(PLG)](https://www.notion.so/p/k8s-折腾记使用-loki-统一管理集群日志 plg/)

使用 Hugo 构建
主题 StackJimmy 设计