本文由 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 端口红了。
|
|
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 超时
|
|
从 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.com→chGBT 点 comchatgpt.com→charge 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 端口暴露到了公网:
|
|
加了 Basic Auth,没有认证返回 401。Let’s Encrypt 证书 Caddy 自动续,不用管。
闲着也是闲着,还试了下用 DeepSeek 给 ASR 结果做后处理。确实能修一些同音错词——淘定律 → 韬定律——但也可能把原文"合理化改写"。最后决定:后处理默认关,保留原文。语义真实性优先。
上云——StepAudio 2.5 + Speaker Diarization
为什么又换?
Fun-ASR-Nano 跑了三周,发现了两个问题:
- 长音频时间戳覆盖异常——超过 30 分钟的音频,时间戳就开始漂移
- 说话人识别不行——多人对话场景,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。
当前架构总览
|
|
总结
半年,一台 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/)