1. 从一场两小时的会议录音说起
上周帮一个做企业培训的朋友处理了一批会议录音,六段音频,每段平均一个半小时。他的需求很朴素:把会上的讨论内容转成文字,方便后续整理成培训纪要。我一开始想的是找几个同事分头听打,后来一算时间成本,果断放弃。转头用腾讯云AI语音识别搭了一套自动化流程,从上传音频到拿到带标点的文字稿,全程不到二十分钟。
这件事让我意识到,会议小帮手这个需求其实非常普遍——不管是团队周会、客户沟通记录,还是培训讲座的复盘,把语音高效转成可检索、可编辑的文字,是很多职场人的刚需。腾讯云AI语音识别提供的正是这样一套能力:你给它一段音频,它还你一份结构化的文本,支持实时流式和录音文件两种模式,覆盖中文普通话、英语、粤语等多种语言,还能自动加标点、做顺滑处理、区分说话人。
这篇文章适合两类人看:一类是完全没有语音识别开发经验、但想快速搭一个会议转写工具的开发者;另一类是用过基础功能、但想搞清楚参数怎么调、坑怎么避的进阶用户。我会从选型逻辑讲到代码落地,再到实测中遇到的真实问题和优化思路,尽量把每一步的“为什么”说清楚。
2. 为什么是腾讯云AI语音识别,而不是其他方案
2.1 会议场景对语音识别的四个硬要求
在动手写代码之前,我先花时间梳理了会议转写这个场景到底需要什么。不是所有语音识别都适合开会用,会议场景有几个很特殊的地方。
第一是长音频处理能力。一场会动辄一小时起步,普通语音识别接口对音频时长有限制,超过就得切片,切片又会带来上下文断裂的问题。第二是说话人区分。会议上多人交替发言,如果转出来的文字分不清谁说的,整理纪要时基本没法用。第三是标点和顺滑。口语里大量“嗯”“那个”“就是说”,不加处理直接转出来,可读性极差。第四是热词定制。每个公司都有自己的业务术语、产品代号、人名,通用模型对这些词的识别准确率往往不理想。
把这四点列清楚之后,选型方向就明确了:我需要一个支持长音频、能区分说话人、自带标点顺滑、并且允许自定义热词的语音识别服务。
2.2 录音文件识别与实时语音识别的分工
腾讯云AI语音识别主要提供两条产品线:录音文件识别和实时语音识别。这两个不是替代关系,而是互补关系,用错了场景会事倍功半。
录音文件识别适合“事后转写”——会议已经开完了,你手里有一段完整的音频文件,提交上去异步处理,等结果返回。它的优势是准确率高、支持说话人分离、支持热词表,缺点是异步的,提交后需要轮询或等回调。实时语音识别适合“边开边转”——会议进行中,音频流实时上传,文字实时返回。它的优势是延迟低,适合做会议实时字幕,缺点是对网络稳定性要求高,且部分高级功能(如说话人分离)在实时模式下支持有限。
我的建议是:如果目的是整理会议纪要,优先用录音文件识别。因为纪要本身是会后工作,没必要追求实时,而录音文件识别在准确率和功能完整度上明显更优。实时语音识别更适合做会议直播字幕、远程协作的实时记录这类场景。
2.3 一个容易被忽略的选型维度:计费模式
很多人选型时只看功能,不看计费,结果项目跑起来才发现成本失控。腾讯云AI语音识别的录音文件识别按音频时长计费,实时语音识别按并发路数或时长计费。对于会议转写这种“批量、非实时”的场景,录音文件识别的成本通常更低。
这里有个实操经验:先把音频做一次静音检测和降噪预处理,再提交识别。会议录音里经常有大段沉默、茶歇、设备调试的空白段,这些时长也是要计费的。我一般会用工具先切掉静音段,实测能省下15%到30%的时长费用。这个细节官方文档不会强调,但长期跑下来差距很明显。
3. 跑通第一个会议转写Demo的完整路径
3.1 开通服务与获取密钥的正确姿势
第一步是在腾讯云控制台开通语音识别服务。这里有个坑要注意:语音识别和语音合成是两个独立服务,别开错了。开通之后进入访问管理创建密钥,你会拿到SecretId和SecretKey。
密钥管理上我踩过一次坑。一开始图省事,把密钥硬编码在代码里,后来代码传到代码仓库,密钥就泄露了。正确的做法是用环境变量或者密钥管理服务。我在本地开发时用.env文件配合python-dotenv加载,生产环境则用云厂商提供的密钥管理能力。下面是一个安全的读取方式:
import os from dotenv import load_dotenv load_dotenv() SECRET_ID = os.getenv("TENCENT_SECRET_ID") SECRET_KEY = os.getenv("TENCENT_SECRET_KEY")注意:SecretKey一旦泄露,别人可以用你的账号调用所有已开通的服务,产生费用甚至数据风险。养成密钥不落代码库的习惯,比事后补救重要得多。
3.2 录音文件识别的请求结构拆解
录音文件识别的调用逻辑是:构造请求参数,提交任务,拿到TaskId,然后轮询查询结果。请求参数里几个关键字段需要重点理解。
EngineModelType是引擎模型类型,会议场景一般用16k_zh(16k采样率中文通用模型)。如果会议里有较多英文夹杂,可以考虑16k_zh_en这类中英混合模型。ChannelNum是声道数,单声道填1,双声道填2。ResTextFormat是返回文本格式,填0返回基础文本,填1返回带时间戳的详细结果,填2返回带说话人分离的结果。SpeakerDiarization开启说话人分离,SpeakerNumber指定说话人数量(不指定则自动判断)。
这里有个参数组合的坑:说话人分离和时间戳不是所有引擎模型都支持。我一开始用了一个不支持的模型,提交后一直报参数错误,排查了半天才发现是模型选错了。建议在控制台先查清楚当前支持的模型列表,再决定参数组合。
3.3 提交任务与轮询结果的代码实现
下面是一段可直接运行的Python代码,演示从提交到获取结果的完整流程。我用的是腾讯云官方SDK,封装程度比较高,省去了手动签名的麻烦。
import json import time from tencentcloud.common import credential from tencentcloud.common.profile.client_profile import ClientProfile from tencentcloud.common.profile.http_profile import HttpProfile from tencentcloud.asr.v20190614 import asr_client, models def create_client(secret_id, secret_key): cred = credential.Credential(secret_id, secret_key) http_profile = HttpProfile() http_profile.endpoint = "asr.tencentcloudapi.com" client_profile = ClientProfile() client_profile.httpProfile = http_profile return asr_client.AsrClient(cred, "ap-guangzhou", client_profile) def submit_task(client, audio_url): req = models.CreateRecTaskRequest() params = { "EngineModelType": "16k_zh", "ChannelNum": 1, "ResTextFormat": 2, "SourceType": 0, "Url": audio_url, "SpeakerDiarization": 1, "SpeakerNumber": 0, "FilterDirty": 1, "FilterModal": 1, "ConvertNumMode": 1 } req.from_json_string(json.dumps(params)) resp = client.CreateRecTask(json.dumps(params)) return json.loads(resp.to_json_string())["Data"]["TaskId"] def query_result(client, task_id): while True: req = models.DescribeTaskStatusRequest() req.from_json_string(json.dumps({"TaskId": task_id})) resp = json.loads(client.DescribeTaskStatus(req).to_json_string()) status = resp["Data"]["Status"] if status == 2: return resp["Data"]["Result"] elif status == 3: raise Exception("识别失败: " + resp["Data"]["ErrorMsg"]) time.sleep(5)这段代码里,FilterDirty和FilterModal控制是否过滤脏词和语气词,ConvertNumMode控制是否把数字转成阿拉伯数字。会议纪要场景我一般把这三个都打开,转出来的文字更干净。
3.4 音频格式与采样率的预处理要点
腾讯云录音文件识别支持的音频格式包括wav、mp3、m4a、aac等,但采样率必须匹配引擎模型。用16k模型就得传16k采样率的音频,传8k的会识别不准,传48k的虽然能提交但会被重采样,可能引入失真。
我的处理流程是:拿到原始录音后,先用ffmpeg统一转成16k、单声道、wav格式。命令很简单:
ffmpeg -i input.m4a -ar 16000 -ac 1 -c:a pcm_s16le output.wav这一步看起来多余,但实测下来,统一格式后的识别准确率比直接传原始格式稳定得多。尤其是手机录的会议音频,采样率五花八门,不统一处理很容易出问题。
4. 让转写结果真正能用的四个关键处理
4.1 说话人分离结果的结构化重组
开启说话人分离后,返回的结果里每个句子会带一个SpeakerId。但原始返回是扁平的句子列表,直接看很乱。我写了一个重组函数,把同一个说话人的连续发言合并成段落,并给每个说话人分配一个可读的标签(如“说话人1”“说话人2”)。
def restructure_by_speaker(sentences): paragraphs = [] current_speaker = None buffer = [] for sent in sentences: speaker = sent.get("SpeakerId", 0) text = sent["FinalSentence"] if speaker != current_speaker: if buffer: paragraphs.append({ "speaker": current_speaker, "text": "".join(buffer) }) current_speaker = speaker buffer = [text] else: buffer.append(text) if buffer: paragraphs.append({"speaker": current_speaker, "text": "".join(buffer)}) return paragraphs重组之后,输出就是“说话人1:……”“说话人2:……”这样的格式,整理纪要时直接能用。这里有个经验:说话人数量不要设死。我试过指定SpeakerNumber为固定值,结果遇到实际人数不符时,识别会把多个人的话归到一个人头上。设成0让系统自动判断,虽然偶尔会多分出几个说话人,但比归错人要好。
4.2 热词表让专业术语识别率翻倍
这是我认为投入产出比最高的一个优化。会议里经常出现公司内部的产品名、项目代号、人名,通用模型对这些词的识别基本靠猜。腾讯云支持自定义热词表,你可以把常用术语列进去,识别时会优先匹配。
热词表的创建在控制台完成,每个词可以设置权重。我的经验是:权重不要设太高。一开始我把所有词权重都拉到10,结果发现一些常用词被误匹配成了热词。后来调整策略,核心术语权重设8到10,一般术语设5到6,效果明显更稳。
热词表更新后不是立即生效的,需要等几分钟。我一般会在会议前提前更新好,避免临时抱佛脚。
4.3 标点与顺滑处理的参数取舍
FilterDirty、FilterModal、FilterPunc这几个参数控制文本清洗程度。全开的话,转出来的文字很干净,但可能丢失一些语气信息。比如“这个方案我觉得不太行”和“这个方案我觉得不行”,语气强度是不一样的,全开过滤后可能都变成后者。
我的做法是:正式纪要场景全开,访谈记录场景只开脏词过滤。因为访谈记录需要保留受访者的原始表达,语气词本身也是信息。这个取舍没有标准答案,取决于你的下游用途。
4.4 时间戳对齐在纪要定位中的妙用
开启时间戳后,每个句子会带StartTime和EndTime(毫秒)。这个信息在整理纪要时非常有用。比如你想回听某段讨论的原始录音,直接根据时间戳定位到音频的对应位置,不用从头听。
我甚至用这个做了一个小功能:把转写文本和音频播放器联动,点击某句话就跳到对应音频位置。实现思路是把时间戳转成秒,传给播放器的currentTime。这个功能在复核转写准确性时特别方便。
5. 实测中暴露的问题与排查链路
5.1 长音频提交后一直处于“处理中”
第一次跑一个两小时的录音,提交后状态一直是“处理中”,等了快四十分钟还没结果。我一开始以为是服务卡了,后来查文档才发现,录音文件识别的处理时长和音频时长、排队情况都有关,高峰期排队是正常的。
排查这个问题的正确链路是:先确认TaskId是否有效,再查任务状态码。状态码1是等待中,2是处理中,3是已完成,4是失败。如果长时间停在1或2,大概率是排队,不是故障。我的应对策略是:提交后不要死等,用回调或者定时轮询。轮询间隔设5到10秒比较合理,太频繁没必要,太慢又影响体验。
5.2 识别结果里出现大量重复句子
有一次转出来的文字里,同一句话重复出现了三四遍。我第一反应是代码bug,检查了重组逻辑没发现问题。后来对比原始音频才发现,那段录音有回声,识别引擎把回声也当成了独立语音。
这个问题的根因在音频质量,不在识别服务。解决办法有两个:一是提交前做降噪和去回声处理,ffmpeg的afftdn滤镜可以做一些基础降噪;二是如果条件允许,用带指向性的麦克风重新录制。软件层面的补救永远不如源头录好。
5.3 说话人分离把一个人拆成两个
这个坑我遇到不止一次。会议里某个人说话声音忽大忽小,或者中途换了位置离麦克风远了,识别就会把他拆成两个说话人。这不是bug,是声学特征变化导致的正常现象。
应对方法:在重组阶段做后处理合并。如果两个说话人的发言在时间上连续、且没有其他人插入,可以判断为同一人。我写了一个简单的启发式规则:相邻说话人段落之间时间间隔小于500毫秒,且文本语义连贯,就合并。这个规则不能保证100%准确,但能解决大部分误拆分。
5.4 网络超时与重试机制的设计
调用云服务绕不开网络问题。我遇到过提交任务时超时,但任务其实已经提交成功了,重试又提交了一次,导致重复计费。后来我加了幂等设计:每次提交前生成一个唯一请求ID,服务端根据这个ID去重。腾讯云的部分接口支持ClientToken参数,用上它就能避免重复提交。
重试策略上,我设置的是指数退避:第一次失败等1秒,第二次等2秒,第三次等4秒,最多重试3次。超过3次就记录日志人工介入,不要无限重试。
6. 从Demo到日常工具还差哪些工程化改造
6.1 批量任务队列与并发控制
单个会议转写跑通之后,下一步是支持批量处理。我一开始用简单的for循环串行提交,十个音频跑了一下午。后来改成任务队列加并发控制,效率提升明显。
并发数不是越高越好。腾讯云对并发有配额限制,超了会报错。我的做法是:先查配额,再把并发数设为配额的80%,留出余量应对突发。队列用Redis或者简单的内存队列都行,关键是做好失败重试和状态追踪。
6.2 转写结果的存储与检索设计
转写出来的文本如果只是存成txt,后续检索很麻烦。我把它存进了支持全文检索的数据库,每条记录包含会议ID、日期、参与人、转写文本、时间戳。这样后续想找“上次会上谁提到了某个方案”,直接搜关键词就行。
存储时有个细节:原始结果和处理后的结果分开存。原始结果保留JSON格式,方便以后用新逻辑重新处理;处理后的结果存文本,方便直接展示。这样既不丢信息,又保证可用性。
6.3 成本监控与用量告警
跑了一段时间后,我发现费用比预期高。排查后发现是有几个测试音频反复提交了很多次。后来我加了用量监控:每天统计提交的音频总时长,超过阈值就发告警。腾讯云控制台本身有费用告警功能,设置一个月度预算,快超了会通知。
这里分享一个省钱技巧:测试阶段用短音频。我一开始拿完整会议录音做测试,一次就是几十分钟的计费。后来改成截取前两分钟做功能验证,验证通过再跑完整音频,测试成本降了一个数量级。
6.4 敏感信息过滤与数据安全
会议内容可能涉及内部信息,转写文本的存储和传输要注意安全。我的做法是:传输全程用HTTPS,存储时对敏感字段加密,访问加权限控制。腾讯云的对象存储和数据库都支持加密,配置一下就行。
另外,转写完成后及时清理临时音频文件。音频文件体积大,长期堆积既占空间又有泄露风险。我设置了一个定时任务,转写成功七天后自动删除原始音频,只保留文本结果。
7. 一些不在文档里的实操心得
热词表要定期维护。业务在变,术语也在变,我每个月会回顾一次识别错误的词,把高频错词加进热词表。这个习惯坚持下来,识别准确率的提升是肉眼可见的。
音频质量决定上限。再好的识别引擎,遇到嘈杂的录音也无力回天。如果会议转写是刚需,建议在会议室硬件上做点投入,一个几百块的指向性麦克风,效果比后期任何软件处理都好。
不要追求100%准确。语音识别本质是概率模型,总会有错字。我的目标是让准确率达到“不用回听就能理解大意”的程度,剩下的细节靠人工校对。把预期放合理,工具才用得舒服。
最后说一个我常用的调试方法:拿一段自己朗读的音频做基准测试。自己读的音频,内容已知,识别错了立刻能发现。每次调整参数后跑一遍基准测试,就能判断改动是正向还是负向。这个方法帮我省了很多盲目调参的时间。