背景与痛点
做 ASR 最怕三件事:
- 模型一上线,GPU 内存狂飙,延迟飙到 600 ms 以上;
- 换个小语种,词错率直接掉 15%;
- 老板一句“要实时字幕”,开发组集体加班。
传统方案里,TensorFlow 的 SavedModel + gRPC 确实稳,但编译一次半小时;PyTorch 的 torchscript 又常常算子不支持,得自己写 C++ 扩展。部署链路一长,只要一个环节掉链子,全链路跟着抖。
技术选型:为什么把 ComfyUI 拉进来?
一句话:ComfyUI 把“流程可视化 + 节点即插件”玩明白了。
| 维度 | TensorFlow | PyTorch | ComfyUI |
|---|---|---|---|
| 开发范式 | 代码即流程 | 代码即流程 | 节点拖拽 |
| 推理后端 | TF-Lite / TF-Serving | TorchScript / libtorch | onnxruntime / tensorrt |
| 插件生态 | 少 | 中 | 极多(音频、CV、LLM 全打通) |
| 热更新 | 重启服务 | 重启服务 | 刷新页面即可 |
| 学习曲线 | 陡峭 | 陡峭 | 有手就行 |
ComfyUI 本身不是为 ASR 生的,但它把“模型加载-前处理-推理-后处理”拆成节点,正好把 ASR 的整条链路解耦。节点可以塞 Python 脚本,也能直接挂 onnx,等于白捡一个可视化 Pipeline 框架。
核心实现:30 分钟搭一条 ASR 链路
1. 节点规划
- AudioLoader:读麦克风或 wav,统一重采样 16 kHz
- VAD:用 silero-vad,先剪掉静音,减少 30% 计算量
- FeatureExtraction:mel 80 维,25 ms 窗移 10 ms
- ASRModel:onnx 格式的 whisper-small,int8 量化
- PostProcess:时间戳对齐、热词替换、顺滑输出
2. 节点 → Python 脚本
ComfyUI 的节点就是 Python 类,继承comfyui.node.NODE,核心就三件事:
- INPUT_TYPES:告诉前端需要啥参数
- FUNCTION:真正干活的函数
- RETURN_TYPES:输出给下一节点
把上面五个节点写成五个脚本,扔进custom_nodes/asr_nodes/即可。
3. 保存模板
拖拽完一次,右上角 Export → json,以后一条命令拉起:
python main.py --asr-pipeline asr_live_subtitle.json换模型只改一个节点,0 UI 操作。
代码示例:关键片段
下面给出 FeatureExtraction + ASRModel 两段,直接粘到 custom_nodes 就能跑。
# asr_feature_node.py import numpy as np import librosa from comfyui.node import NODE class ASRFeatureNode(NODE): @classmethod def INPUT_TYPES(cls): return {"required": { "audio": ("AUDIO",), "sr": ("INT", {"default": 16000})}} RETURN_TYPES = ("MEL",) FUNCTION = "extract" def extract(self, audio, sr): # audio: tuple (sr, np.ndarray) src_sr, y = audio if src_sr != sr: y = librosa.resample(y, orig_sr=src_sr, target_sr=sr) mel = librosa.feature.melspectrogram( y=y, sr=sr, n_fft=400, hop_length=160, n_mels=80) logmel = librosa.power_to_db(mel, ref=np.max) return (logmel.T,) # shape (T, 80)# asr_inference_node.py import onnxruntime as ort import numpy as np class ASRInferenceNode(NODE): def __init__(self): self.ort = ort.InferenceSession("whisper_small_int8.onnx", providers=['CUDAExecutionProvider']) @classmethod def INPUT_TYPES(cls): return {"required": {"mel": ("MEL",)}} RETURN_TYPES = ("TEXT",) FUNCTION = "decode" def decode(self, mel): # mel: (T, 80) mel = mel[None, :, :] # add batch logits = self.ort.run(None, {"mel": mel.astype(np.float32)})[0] text = self.ctc_decode(logits) # 简化:greedy return (text,)两段代码加起来不到 80 行,就把“特征 → 模型 → 文字”跑通。
性能优化:让 300 ms 降到 80 ms
- 批处理:VAD 切段后,凑够 8 段再扔 GPU,吞吐量 x4,延迟只增 20 ms。
- int8 量化:whisper 官方给出校准集,onnxruntime 自带
quantize_dynamic.py,模型从 466 MB → 129 MB,RTF=0.06。 - 流式推理:把 30 s 长音频切成 3 s 窗口,overlap 0.5 s,cache 的 key/value 用 ComfyUI 的“隐式状态节点”存下来,下次直接续跑。
- TensorRT:相同 onnx 图,trtexec 编译后,首帧延迟再降 18%。
避坑指南:上线三天踩出的雷
- 内存泄漏:onnxruntime 的
InferenceSession默认线程池不释放,节点里加__del__手动session.release()。 - 线程竞争:ComfyUI 前端刷新会触发后端重新实例化,加
threading.Lock()防止重复加载模型。 - 采样率不一致:安卓手机录的 48 kHz 直接扔给 VAD,结果全被当成静音,统一在 AudioLoader 里重采样。
- 热词不生效:whisper 的解码器走 timestamp 模式,热词替换得在 PostProcess 节点做,别在 logits 层瞎改。
总结与展望
用 ComfyUI 搭 ASR,最大的收获是“把链路拆成乐高”。模型、特征、业务规则全解耦,换模块就像换显卡一样爽。下一步打算把 LLM 纠错节点也接进来,让 whisper 先出草稿,再用 3B 小模型做本地纠错,延迟控制在 +50 ms 内。
如果你也在为 ASR 的部署、实时、准确率头疼,不妨把 ComfyUI 当胶水先跑通 MVP,再逐块替换性能瓶颈。工具只是工具,能让团队少熬一个通宵,就是好东西。