
这个系列写到第四天轮到最容易被低估的一块语音识别音频转文字。很多人第一反应是这不早成熟了吗随便调个接口就行真上手才发现一段带背景噪声的会议录音丢进去出来的文字能把人看笑——人名全是同音字数字全成了汉字标点全靠猜长音频还会莫名其妙截断。语音识别这条链路表面上是音频进、文字出中间其实是信号处理、声学建模、语言解码、文本规整四套东西在接力任何一环参数不对后面全歪。这篇就把我这几天摸索出来的完整路径摊开讲从一段音频进来先做什么、技术路线怎么选、采样率和分片这些参数为什么这么定、代码怎么写、准确率上不去该从哪儿查。不管你是刚接触音频转文字的新手还是已经调过几个接口但结果总不理想的开发者应该都能从里面抠到点能直接用的东西。1. 先想清楚音频转文字到底在转什么1.1 声波到文字的四个接力环节很多人把语音识别当成一个黑盒函数audio in → text out中间发生什么一概不管。这个认知在跑 demo 的时候没问题一旦要调优就会卡死因为你不知道该动哪一层。我习惯把它拆成四段来看每一段的输入输出都很明确出问题时能快速定位到底是哪一段烂了。第一段是前端信号处理。原始音频进来先做增益归一化、降噪、去混响、静音检测VAD。这一步的目标不是让音频好听而是让后面的模型好认。模型训练时见到的基本都是干净、音量适中、单声道的语音你给它一段音量只有 -40dBFS 的远场录音它照样按训练分布去理解结果就是大面积识别成空白或者乱码。VAD 的作用是把长音频切成有语音的片段跳过纯静音既省算力也避免模型在静音段幻听出文字。第二段是特征提取。模型不直接吃波形采样点而是吃频谱特征。工业上最常用的是 FBank梅尔滤波器组特征帧长一般 25 毫秒帧移 10 毫秒。这两个数字不是随便定的25ms 大约覆盖 3 到 4 个语音周期能捕捉到一个音素的核心信息10ms 的帧移保证了相邻帧之间有足够重叠不会漏掉快速变化的辅音。你可以算一下16kHz 采样率下25ms 就是 400 个采样点10ms 是 160 个点。一段一小时的音频换算下来是 36 万帧每帧取 40 维 FBank就是 1440 万个浮点数用 float32 存大约 57MB。这个量级决定了你在做批量转写时内存该怎么规划。第三段是声学建模与解码。声学模型负责把每一帧特征映射成音素或者子词的概率分布解码器再结合语言模型把这些概率串成最可能的文字序列。早期是 HMM-GMM后来是 DNN-HMM现在是端到端的 CTC、Attention Encoder-Decoder、RNN-T 三大流派。CTC 的好处是不需要帧级对齐标注训练简单缺点是输出独立性假设强容易出重复字Attention 类模型语言建模能力强但需要额外处理对齐RNN-T 兼顾流式和精度工程上最受欢迎但实现复杂度也最高。你在选型时看到的流式识别实时字幕本质上就是 RNN-T 或者 chunk-based 的 CTC 在支撑。第四段是后处理。模型吐出来的原始结果通常是没有标点、数字全是汉字、专有名词全靠音近、没有时间戳、多个说话人混在一起。标点恢复模型、逆文本规整ITN、热词替换、说话人分离都是在这一层做的。很多人抱怨识别不准其实模型本身没错是后处理没做把二零二四年三月直接丢给下游系统人家当然对不上。1.2 三条技术路线怎么选别一上来就冲最贵的搞清楚了链路接下来是选型。市面上能落地的路线基本就三条我把它们的实际表现整理成了一张表这张表是我踩了几次坑之后总结的比官方宣传页靠谱。维度云端识别接口开源端到端模型轻量离线引擎中文准确率高尤其带热词和行业模型中等偏上看模型规模中等小模型天花板明显部署成本按量计费无硬件投入需要 GPU显存 4G 起步CPU 可跑内存占用低单次延迟网络往返 排队通常 300ms 起本地推理可做到 100ms 内极低适合嵌入式长音频支持一般有单文件时长上限自己控制无上限需要自己分片数据流向音频出本机全程本地全程本地定制能力热词表、少数支持模型微调可微调、可换语言模型定制空间小适合场景快速验证、非敏感业务有数据合规要求、需深度定制端侧、离线、资源受限我自己的选择逻辑是这样的如果只是验证产品可行性直接用云端接口半天就能出结果别在环境上耗时间如果有数据不出本地的硬性要求或者要针对特定领域的术语做深度优化那就上开源端到端模型如果是端侧设备、树莓派这类资源受限的环境轻量离线引擎是唯一解但你要接受准确率打折。这里有个容易忽略的点不要拿云端接口的准确率去要求离线小模型。我见过有人拿一段带口音的访谈音频云端出来 95% 正确换本地小模型掉到 78%然后认定本地方案不能用。实际上小模型在安静环境、标准普通话下的表现能到 90% 以上差距主要出在噪声鲁棒性和语言模型规模上。选型时要按你的真实音频条件去测而不是拿最难的样本去卡所有方案。2. 音频预处理决定识别上限的一步2.1 采样率、声道、位深这几个参数错一个全盘皆输我在这一块栽过最狠的一次拿到一批客服录音直接丢进识别接口出来全是嗯啊和断句错误的短词。查了两个小时最后发现源文件是 8kHz 采样、电话线路录音而模型是按 16kHz 训练的。工具链没有报错因为它会自动做重采样但 8kHz 的上限频率只有 4kHz汉语里大量辅音s、sh、f、x的能量集中在 4kHz 以上这部分信息在录音阶段就已经永久丢失了任何重采样都补不回来。这不是代码问题是物理限制。所以拿到音频的第一件事永远是先探测参数再决定怎么处理。要看的四个指标采样率16kHz 是语音识别的工业标准8kHz 是电话音质44.1kHz 和 48kHz 是音乐标准。高于 16kHz 要做降采样低于 16kHz 无法补救只能接受准确率损失。声道数模型基本都吃单声道。立体声要混成单声道混音方式建议直接取两声道平均而不是简单取左声道因为有些录音设备只有一个声道有信号。位深16bit 的动态范围是 96dB对语音完全够用。遇到 24bit 或 32bit 浮点的文件转成 16bit 不会有可感知损失反而能减小文件体积。编码格式mp3、aac 这类有损压缩在高码率下问题不大但低码率比如 32kbps会明显损伤高频。遇到低码率压缩音频识别前没必要再转一次有损格式直接转成 16bit PCM 的 wav 最稳。把这四条定下来你的音频输入就标准化了。我用 Python 标准库就能做基本检查不需要装额外的重依赖import wave def inspect_audio(path): with wave.open(path, rb) as f: channels f.getnchannels() sampwidth f.getsampwidth() # 字节数2 表示 16bit framerate f.getframerate() nframes f.getnframes() duration nframes / framerate print(f声道: {channels}, 位深: {sampwidth * 8}bit, f采样率: {framerate}Hz, 时长: {duration:.2f}s) return { channels: channels, sampwidth: sampwidth, framerate: framerate, duration: duration, }如果探测出来是 48kHz 立体声就需要转成 16kHz 单声道。转的时候有个细节降采样之前必须先做低通滤波否则高于 8kHz 的频率会折叠回低频段产生混叠失真听起来像加了金属噪音识别率会明显下降。自己写线性插值降采样是省事但抗混叠效果差正式项目建议用带滤波的专业重采样库或者干脆用命令行工具一次性处理# 批量把目录下的音频统一成 16kHz / 单声道 / 16bit for f in ./raw_audio/*.mp3; do name$(basename $f .mp3) ffmpeg -i $f -ac 1 -ar 16000 -sample_fmt s16 ./clean/${name}.wav done-ac 1是单声道-ar 16000是采样率-sample_fmt s16是 16bit 有符号整型。这三刀下去输入就干净了。2.2 降噪和分片什么时候该做什么时候做了更糟降噪这件事我的态度是能不降就不降。原因很直接现在的识别模型在训练时已经见过大量带噪数据本身有一定鲁棒性而大多数传统降噪算法谱减法、维纳滤波在压制噪声的同时会把语音的低能量部分一起削掉尤其是清辅音反而让模型更认不出来。我做过对比测试同一段地铁环境录音原始音频识别字错率 18%过了某开源降噪工具之后反而升到 24%。那什么时候该降两种情况一是噪声是稳态的比如空调声、风扇声、电流底噪这类可以用简单的频域滤波处理二是信噪比低到模型直接放弃比如人声被音乐完全盖住这时候不降根本没救降了至少能捞回一部分。判断标准很实用先跑一遍原始音频看识别结果里有没有成段的空白或者大段重复有就说明模型被噪声带偏了再考虑降噪。分片比降噪重要得多。长音频识别有三个现实约束接口有单文件时长上限、模型有输入长度限制、失败重试的代价高。我的经验是按 20 到 30 秒切太短会导致上下文不足、标点断得稀碎太长则单次推理慢、出错重传成本高。切片时不能硬切要按静音点切并且相邻片段之间留 0.3 到 0.5 秒重叠。重叠这件事我特别说一下。假设一句话正好跨在切片边界上今天下午三点开会切成了今天下午三和点开会两段各自识别出来是今天下午三和点开会合并的时候要么丢字要么重复。留了重叠之后边界处的词会在两段里都出现合并时按文本相似度做去重就能接上。去重的实现不用太复杂我一般取前一段末尾 N 个字符和后一段开头 N 个字符做最长公共子串匹配N 取 10 到 15 就够。静音检测VAD有个坑要提醒不要用固定能量阈值。同一个会议录音有人离麦克风近有人远固定阈值会把远场说话人的语音当成静音切掉。用自适应阈值或者直接用轻量的模型 VAD虽然多花一点算力但漏检率能降一个量级。切出来的片段顺手记一下起止时间后面加时间戳的时候直接能用省得再对一遍。3. 从零跑通第一条识别结果3.1 环境准备先把依赖和模型文件理清楚环境这块我踩的坑主要不在装包而在模型文件的存放和版本匹配。开源识别框架通常由三部分组成声学模型、语言模型、配置文件或者叫解码图。这三者的版本必须对齐混用不同版本的文件轻则识别结果乱码重则直接报维度不匹配。我现在的习惯是建一个独立的模型目录用版本号命名跑之前先做一次小样本自检。目录结构大致这样project/ ├── models/ │ └── asr-v1.2/ │ ├── acoustic.bin │ ├── language.bin │ └── config.yaml ├── clean/ # 预处理后的 16k 单声道 wav ├── out/ # 识别结果 └── transcribe.py依赖安装方面除了识别框架本身我还建议装两个东西一个是音频读取库用来处理非 wav 格式一个是进度条库批量跑几百个文件的时候没有进度提示你会怀疑程序是不是卡死了。这两个都是小依赖但对调试体验影响很大。自检的方法很简单准备一段 3 到 5 秒的清晰人声跑一遍看结果是不是接近原文。如果这段都识别不出来别急着调参数先回去检查模型文件是不是完整、采样率是不是对、音量是不是太低。我有一次折腾了半小时参数最后发现是模型文件下载不完整校验和都对不上。3.2 整段识别最小可运行版本先给一个非流式的最小实现。这里我把识别引擎抽象成一个统一接口具体用哪家、哪个框架换成对应的调用即可重点看流程和参数。import hashlib import json import os from pathlib import Path def file_fingerprint(path): 用文件内容哈希做缓存键避免重复识别同一个文件 h hashlib.md5() with open(path, rb) as f: for chunk in iter(lambda: f.read(1 20), b): h.update(chunk) return h.hexdigest() def transcribe_file(audio_path, engine, cache_dir./cache): fp file_fingerprint(audio_path) cache_file Path(cache_dir) / f{fp}.json if cache_file.exists(): return json.loads(cache_file.read_text(encodingutf-8)) result engine.transcribe( audio_path, sample_rate16000, channels1, languagezh, enable_punctuationTrue, # 打开标点恢复 enable_itnTrue, # 打开逆文本规整把汉字数字转阿拉伯数字 enable_timestampTrue, # 输出字/词级时间戳 hotwords[某某系统, 调度中心], # 领域热词 ) cache_file.parent.mkdir(parentsTrue, exist_okTrue) cache_file.write_text(json.dumps(result, ensure_asciiFalse, indent2), encodingutf-8) return result这段代码里有三个设计是我实际用下来觉得必须有的。第一是文件哈希缓存。批量转写最怕重复跑调一次参数全量重跑一遍时间成本极高。用文件内容哈希做键参数没变就直接读缓存改参数时把参数拼进哈希里就能自动失效。我用这个办法把一个 200 个文件的任务从每次 40 分钟压到了首次 40 分钟、后续秒开。第二是热词表。这是提升专有名词准确率性价比最高的手段没有之一。一段技术会议录音里微服务容器编排这类词模型本来认识但某某中台调度策略 v2这种内部叫法不加热词几乎必错。热词一般支持几十到几百个我建议按业务场景分组维护别一股脑全塞进去热词太多反而会干扰正常词。第三是逆文本规整ITN。模型原始输出是二零二四年三月十五日下午三点半ITN 之后是2024年3月15日下午3点半。如果你的下游要做实体抽取、要做时间解析这一步不开后面全是坑。有些框架默认不开要显式传参。3.3 后处理把能看变成能用原始识别结果到能交付的文本中间还有几步。我按重要性排个序。标点恢复是第一位。没有标点的长文本人读起来累程序处理起来更麻烦。现在的框架大多自带标点模型但要注意它会受音频质量影响背景噪声大的时候标点会乱加。我的做法是识别完之后做一次规则清洗连续两个以上句号合并成一个、句首不加逗号、引号配对检查。时间戳对齐是第二位。只要涉及字幕、会议纪要定位、音频检索就必须有。理想的时间戳是词级的退一步字级也行。这里有个细节分片识别之后每段的时间戳都是相对片段起点的合并时要加上片段的绝对偏移。这一步很容易漏漏了之后所有字幕都从零开始越往后偏得越离谱。说话人分离是第三位也是最难的一位。简单场景用聚类思路先提取每段语音的声纹特征再做聚类把相似的归为同一个说话人。效果取决于音频质量和说话人数量两人对话场景准确率尚可五六个人开会就很容易串。我的建议是如果业务对说话人标注要求高别自己硬做先用带分离能力的方案跑通再考虑优化。数字和单位规整容易被忽略。模型可能输出三到五个工作日也可能输出3到5个工作日同一个批次里格式不统一下游做统计就麻烦。写一套正则规则统一处理中文数字转阿拉伯数字注意一在一下一起里不能转、百分比统一成%、时间统一成HH:MM格式。4. 准确率上不去按这个顺序查4.1 六类真实原因从数据查到模型识别结果不理想90% 的情况下问题不在模型在数据和流程。我总结了六类原因按排查优先级排优先级现象大概率原因验证方法1整段空白或只有零星字采样率、声道不匹配或音量过低检查音频参数和峰值电平2大量同音错字缺少热词、语言模型不匹配领域加 20 个领域热词重测3长音频后半段丢失单文件超时、分片逻辑错误打印每个分片的起止时间4数字、日期全错未开启 ITN 或规则冲突关闭 ITN 对比原始输出5背景音乐段全是乱码未做音乐检测模型强行识别加音乐/静音过滤6特定口音用户错字率高训练数据口音覆盖不足换更大模型或做微调第一类最好查也最容易被跳过。我见过太多人一上来就调模型参数结果根本原因是音频是 8kHz 电话录音。先看数据再看模型这个顺序不能反。第二类的解决办法最直接加热词。但要注意热词的写法要和实际发音对得上写AI模型可能读成诶爱写人工智能反而更稳。加完热词一定要做 A/B 对比别凭感觉认为加了就好。第三类是工程问题。接口对单文件时长有上限超了会静默截断不报错这个最坑。我的做法是在代码里强制检查时长超过阈值自动切片并且记录每个分片的识别结果长度如果某个分片返回空打日志告警。第五类值得单独说。带背景音乐的音频模型会努力把音乐听成语音输出一堆莫名其妙的字。处理办法是在前端加一个音乐检测判为音乐的片段直接跳过或者标记为非语音。简单实现可以用能量分布的方差判断纯语音的能量变化有明显节奏持续的背景音乐能量更平稳。4.2 三个我反复踩的坑第一个坑是音量归一化做过头。为了让模型听得清有人会把音频拉到接近 0dBFS结果峰值削波波形顶部被切平产生大量谐波失真识别率反而暴跌。正确的做法是控制峰值在 -3dBFS 到 -6dBFS 之间留出余量。第二个坑是分片重叠区去重逻辑写错。我一开始用简单的后一段去掉前 N 个字符结果遇到重复词就多删。后来改成最长公共子串匹配并且限制匹配长度不超过重叠区对应的文本长度才稳定下来。第三个坑是缓存键没包含参数。改了热词表读的还是旧缓存白调了半天以为热词没用。现在我的缓存键是文件哈希 关键参数序列化参数一变自动重算。提示批量任务永远先把 10 个样本跑通、人工核对一遍再全量跑。全量跑完发现格式全错重来的时间成本是你无法接受的。5. 从单文件到批量工程化要考虑的事单文件跑通只是开始真实项目基本都是几百上千小时的音频要处理。这时候要考虑的东西完全不一样。并发控制。识别是典型的 CPU/GPU 密集型任务本地模型并发开太多会显存溢出云端接口并发太高会被限流。我的经验值本地 GPU 单卡并发 1 到 2 路云端接口按服务方的配额留 20% 余量。用队列把任务排起来别用线程池一把梭。断点续传。批量任务跑到 80% 崩了不能从头再来。每个文件识别完立刻落盘用状态文件记录已完成列表重启时跳过已完成的。这个机制看起来简单但省下来的时间是以小时计的。失败重试。区分两类失败可重试的网络抖动、临时限流和不可重试的音频损坏、格式不支持。可重试的做指数退避最多三次不可重试的直接记入失败清单人工处理。别对所有失败都无脑重试会把问题掩盖掉。成本核算。如果用云端接口按音频时长计费跑之前先算清楚总时长 × 单价 × 重跑系数。重跑系数我一般按 1.5 估因为调参阶段必然要重跑几轮。这个数算出来往往会让人重新考虑要不要上本地模型。结果结构化。最终交付的不能是一堆 txt要带元信息文件名、时长、识别时间、置信度、分片信息、使用的模型版本。后面出问题回溯的时候这些字段比文本本身还重要。最后分享一个我在实际项目里的体会语音识别这件事前处理花的时间和后处理花的时间加起来往往是模型调用的三倍以上但它们对最终效果的影响也远大于换个模型。我见过同一段音频换个模型字错率从 15% 降到 13%但把采样率修正、加上热词、做好分片重叠字错率能从 15% 降到 6%。所以如果你刚开始做音频转文字别急着比较各家模型谁的榜单分高先把音频标准化、分片、热词、后处理这四件事做扎实收益比换模型大得多。真要说还有什么后续可以扩展的方向那就是把识别结果喂给下游做结构化抽取比如从会议录音里自动提取待办事项和责任人那才是这套链路真正产生业务价值的地方。