开会两小时,整理纪要一下午,这句话在职场上几乎没人能笑着读完。最近我给自己所在的团队做了一套内部工具,核心就是用腾讯云AI语音识别把会议录音自动转成文字稿,再按说话人拆好段落、清理语气词、生成标准的会议纪要。折腾下来大概用了一周下班后的时间,效果比我预期好不少。这篇文章就完整记录一下这个“会议小帮手”是怎么设计、怎么调通、踩了哪些坑的,给想用类似方案做会议自动化、访谈整理、课程转写的人一个可以直接抄的参考。
这个内容适合三类人看:一类是团队里经常被会议纪要折磨的运营和行政同学,他们可能不写代码,但能看懂方案思路和替代工具;一类是刚接触云上语音识别的开发者,想快速把ASR(自动语音识别)接进自己的业务系统;还有一类是产品经理,想评估“录音转写+结构化输出”这件事的成本和效果。我会把从注册账号到写出第一版可用的转写程序、再到生成格式化纪要的全过程都摊开讲,代码部分用的是Python,思路同样可以平移到其他语言。
1. 项目定位与整体方案选型
1.1 先拆解会议场景的真实痛点
很多人觉得会议纪要就是把录音转成文字,其实完全不是。我见过一个会议只有40分钟,录音转出来的文字稿却有8000多字,里面全是“嗯嗯”“那个”“就是说”,真正有效的决策信息淹没在口语废料里。所以这个项目的第一道坎不是“转写”,而是“转写之后怎么做结构化”。
另外,会议还有一个天然的特殊性:通常不止一个人说话。判断一句话是谁说的,直接影响纪要的价值。如果转写结果只剩一团不分说话人的文字,后期整理的人还是要对着录音单独核对,等于没省多少事。
再往后想一层,纪要还要有“可消费性”。也就是谁来说、说了什么、对应到哪条待办、什么时间完成。这些东西在纯文本转写结果里不会自动出现,需要做规则抽取或后续人工确认。所以我在设计这个项目时,就把目标拆成了三个明确指标:转写准确率能接受、说话人基本可分、待办可被快速提取。后面所有的技术选型都是围绕这三条来做的。
1.2 为什么选用云上ASR而不是本地方案
一开始我也纠结过要不要本地部署开源的语音识别模型,毕竟本地方案看起来数据私密,而且没有按量计费的概念。后来实测了几轮才彻底明白,在会议场景下,本地开源方案有几个短期绕不开的瓶颈:一是中文口语识别效果普遍不如商业ASR,尤其是带口音、中英混杂、有专业术语的会议;二是需要准备GPU环境,而会议转写往往集中在某个时间段爆发,为了峰值去买显卡很不划算;三是维护成本高,模型更新、热词适配都要自己搞。
腾讯云AI语音识别这一类云服务的好处在于它的引擎是持续迭代的,接口背后维护了大规模中文语料,对普通话、常见口音和行业术语的容错能力更强。这边只需要按调用量付费,低峰期不花钱,高峰期可劲跑,成本是弹性的。另外,云服务通常直接提供多声道支持、时间戳、热词、标点恢复等配套能力,这些在本地开源方案里往往要自己造轮子。
当然,云端方案也有它的问题,最现实的就是数据要上传到服务端,很多公司对会议数据外发有顾虑。我的处理方式是:先做脱敏,明确告知参会人录音会被用于内部转写;同时优先选择支持回调、任务结束后即可删除录音的方式;对于高度敏感的会议,宁可不做自动化,也别硬上。这个边界感很重要,不要为了技术效果丢了合规底线。
1.3 整体数据流与模块划分
这个会议小帮手的内核其实是一条清晰的数据流水线:
录音采集 → 音频预处理 → 云端识别任务 → 轮询/回调获取结果 → 文本清洗与说话人拆分 → 规则抽取待办 → 输出格式化纪要
采集端我直接用的手机或会议室现成的录音笔,要求是尽量录成WAV或M4A格式,双人对话时最好能左右声道分离,也就是两个人各用一个麦克风、分别进左右声道。这一步做得好,后面说话人分离会容易很多。
音频预处理要解决的事情比较杂:统一采样率、去掉长静音、适当做响度归一化、必要时分段。因为云端识别对音频格式和时长都有约束,预处理不好会白白浪费调用次数。
云端识别是整个系统的核心,负责把音频变成带时间戳的文本。这里我选的是录音文件识别接口而不是实时语音识别,原因很朴素:会议是录完再整理的场景,不追求边录边出字,用异步任务的方式更稳定,也不用一直维持长连接。
文本后处理反而是最考验细节的部分,后面我会单独用一整节来展开。这里先给结论:不要让用户直接看ASR原始输出,一定要经过清洗、分段、转模板三步,用户体验才会真正起来。
2. 核心接口与关键参数详解
2.1 腾讯云ASR产品矩阵怎么选
腾讯云语音识别对外有好几个入口,第一次接触的人经常搞混。我按场景帮你理一下:
- 一句话识别:适合App里按住说话、短指令识别,音频时长通常在60秒以内,返回很快,适合交互场景。
- 实时语音识别:适合直播字幕、会议实时字幕这种边说话边出结果的需求,需要建立长连接持续上推音频流。
- 录音文件识别:适合离线转写,提交音频文件后异步返回结果,支持双声道、长音频,我最推荐用于会议场景。
- 录音文件识别极速版:在录音文件识别的基础上进一步压缩返回时间,适合对实时性要求更高、音频不是特别长的场景。
我做会议纪要,核心选的是录音文件识别。原因前面说过:不需要实时,但要稳定、准确、支持多声道。录音文件识别返回的结果里能带每个字的起止时间,还能区分声道,这两点直接应对了会议纪要的两个痛点。
另外要提醒一句,接口选型时要仔细看控制台的计费说明。不同引擎、不同调用方式,价格和免费额度都不一样。个人开发先别贪多,用录音文件识别打底就够了,后面确实有实时字幕需求再单独接实时接口。
2.2 录音文件识别请求参数逐项说明
录音文件识别接口的核心参数其实就几个,但每个都影响最终效果,我逐个说一下我的理解。
EngineModelType是引擎模型,会议场景我一般传16k_zh,也就是16kHz采样率的中文普通话模型。如果会议里中英混杂非常严重,可以再看有没有更适合的混合模型。采样率这一栏容易踩坑:你传的采样率必须和你实际音频匹配,我习惯在预处理阶段统一重采样到16kHz,再配合16k_zh,匹配最稳妥。
ChannelNum是声道数。这是会议场景的关键参数:如果录音是单声道,就传1;如果是双声道双麦克风录音,传2。传2之后,返回结果里会单独给出每个声道的转写文本,这对我来说比混在一起再靠声纹去分说话人更可靠。
SourceType表示音频来源方式,我一般用SourceType为0表示本地文件上传,把音频文件转成Base64放在请求体里直接提交。如果音频非常大,也可以先用对象存储托管,再把可访问的URL传给接口,SourceType设为1。两种方式我都试过,短音频用Base64最直接,大音频走URL更省心。
ResTextFormat是识别结果的文本格式。这个参数我之前忽略过,后来发现它直接影响后处理难度。默认返回的是带标点的普通文本;如果希望得到带时间戳的句子级结果,可以换成带时间戳的格式。会议纪要我建议用带句子时间戳的格式,因为后面做“按时间段定位讨论内容”非常有用,比纯文本好用太多。
还有几个辅助参数值得调一下:ConvertNumMode用来控制数字的呈现方式,我一般选成转阿拉伯数字,“三百二十五”变成“325”,阅读体验好很多。FilterDirty和FilterPunc是过滤脏词和标点的开关,如果在文本后处理里已经自己写了清洗规则,接口这里可以不开,留给自己的代码统一处理更灵活。
2.3 结果解析与轮询机制
录音文件识别是异步的。提交任务之后会先拿到一个TaskId,接着要通过DescribeTaskStatus轮询任务状态,等它变成成功后再取结果。轮询的间隔我一般设5到10秒一次,任务通常几十秒内返回,长音频可能需要几分钟。
这里有一个容易忽略的细节:任务完成后的Result是一段JSON字符串,里面内嵌了每个句子、每个字的详情,以及各声道对应的文本。刚开始我图省事只取了个整段ResultText,后来发现这种用法丢失了很多信息。正确做法是完整解析JSON,把句子列表按时间戳排序,再按声道字段把文本分组,这样就能还原出一个多说话人的、有时间线的会议逐字稿。
解析的时候还要处理一个常见问题:极长的音频返回的结果可能非常大,一次响应体装不下。所以我在设计数据存储时就把结果原样存到本地JSON文件里,再做增量解析,避免大字符串在内存里反复拷贝导致卡顿。
3. 实操过程:从建号到跑通第一段会议转写
3.1 账号、密钥和SDK环境准备
实操第一步是在腾讯云控制台开通语音识别服务。实名认证之后,进入语音识别产品页开通即可。之后要在访问管理里创建一组API密钥,也就是SecretId和SecretKey。这里的安全习惯要说清楚:密钥不要直接硬编码在代码里,更不要提交到Git仓库。我一般放在环境变量里,或者用一个单独的配置文件,并且把该文件加入.gitignore。
Python这边安装官方SDK非常简单,一条pip命令就能搞定。SDK会自动处理请求签名、序列化等工作,比自己拼HTTP请求省太多事。如果你所在环境网络受限装不上,也可以直接按官方文档里的签名规则手动请求,但没必要,有SDK就用SDK。
环境准备里还有一个容易被忽略的点:确认接口的地域。我用的录音文件识别接口在多数地域都有,但有些服务只在部分地域开放。建议优先选择和你的对象存储、计算资源同地域的接入点,减少跨地域调用的网络延迟和稳定性问题。
3.2 最小可用代码:一句话识别验证链路
在正式接录音文件识别前,我建议你先用一句话识别把“账号-密钥-SDK-接口”这条链路完整跑通。这个接口最简单,请求一次就同步返回结果,最适合当冒烟测试。代码如下:
import os from tencentcloud.common import credential from tencentcloud.asr.v20190614 import asr_client, models secret_id = os.environ.get("TENCENTCLOUD_SECRET_ID") secret_key = os.environ.get("TENCENTCLOUD_SECRET_KEY") cred = credential.Credential(secret_id, secret_key) client = asr_client.AsrClient(cred, "ap-shanghai") req = models.SentenceRecognitionRequest() req.ProjectId = 0 req.SubServiceType = 2 req.EngSerViceType = "16k_zh" req.SourceType = 1 req.VoiceFormat = "wav" req.Data = base64.b64encode(open("test.wav", "rb").read()).decode("utf-8") req.DataLen = os.path.getsize("test.wav") resp = client.SentenceRecognition(req) print(resp.Data.Text)这段代码如果跑通,说明密钥没问题、SDK没问题、网络没问题。我第一次跑的时候遇到过地域填错导致请求报错的问题,把地域改成ap-shanghai之后就正常了。测试音频建议用一段干净清晰的普通话,先排除录音质量问题,再谈识别效果。
3.3 接录音文件识别:完整转写流程
链路验证完,就可以切到正式使用的录音文件识别。我用的是任务提交加轮询的方式:
import base64 import time req = models.CreateRecTaskRequest() req.EngineModelType = "16k_zh" req.ChannelNum = 1 req.ResTextFormat = 1 req.SourceType = 0 req.Data = base64.b64encode(open("meeting.wav", "rb").read()).decode("utf-8") resp = client.CreateRecTask(req) task_id = resp.Data.TaskId print("task_id:", task_id) status_req = models.DescribeTaskStatusRequest() while True: status_req.TaskId = task_id status_resp = client.DescribeTaskStatus(status_req) status = status_resp.Data.StatusStr if status == "success": result = status_resp.Data.Result break elif status == "failed": raise RuntimeError(f"识别失败: {status_resp.Data.ErrorMsg}") else: time.sleep(5)这里有几个细节值得提:CreateRecTask返回的数据里还有个Result字段,那个只是预判字符串,真正结果在DescribeTaskStatus里。轮询时要对状态做完整判断,success和failed都要覆盖,不能只判断是不是success,否则任务失败时会陷入死循环。我在第一次写的时候没有处理failed分支,结果任务里音频格式有问题,直接卡在那里轮询了半天。
任务成功之后拿到的Result是一个JSON字符串,要把里面的JSON解析出来,重点看SentenceResults字段,那里面是每个句子的详细信息。每个人的句子会有ChannelId标识是哪个声道,StartTime和EndTime标识起止时间。这些数据才是后面做结构化纪要的金矿。
3.4 双声道会议录音的说话人分离
大多数情况下会议纪要是需要区分说话人的。我在采集端鼓励大家用双声道录音:会议桌两侧各放一支麦克风,分别接入录音设备的左右声道。这种原始设计足够让ASR返回按声道区分的文本,我再根据声道标记映射成“发言人A”“发言人B”等角色。
代码层面就是解析每个句子的ChannelId,做分组:
import json result_data = json.loads(result) sentences = result_data.get("SentenceResults", []) speaker_text = {0: [], 1: []} for sentence in sentences: ch = sentence.get("ChannelId", 0) speaker_text[ch].append({ "start": sentence["StartTime"], "end": sentence["EndTime"], "text": sentence["SentenceText"] })这里要做一个重要提示:双声道分人不是万能的。如果会议有五六个人共用一个麦克风,声道分离也只能告诉你“这路麦有人说话了”,至于是谁,没法自动区分。更复杂的方案需要引入说话人声纹识别或结合参会人声纹注册,那是另一个深水区,我这版不展开。
如果采集端没办法做双声道,依然有兜底方案。我尝试过把单声道音频喂给ASR,再用基于文本内容的后处理规则来猜测说话人,比如“刚才那段提到预算的内容大概率是财务说的”。这种做法准确率有限,但结合人工快速修正,也能把整理时间从两个小时压到半小时。
4. 转写结果的文本后处理与纪要生成
4.1 清洗、断句和标点修复
ASR返回的文本虽然带着标点,但它是在识别断句基础上恢复的,跟人正常写作的标点习惯不完全一致。最典型的体现是口语里的“然后然后然后”“那个那个”,以及大量的重复词。直接给用户看这种文本,他们会觉得“AI做的东西还是很机械”。
我在清洗阶段做了三步:第一步,把转折词、语气词、口头禅(“嗯”“啊”“这个”“就是说”)做成一个高频停用词表,按规则批量删除;第二步,把连续重复的片段合并,比如“我们我们下周一”处理成“我们下周一”;第三步,对明显过长的“一句话”按标点重新拆分,把逗号过多的大长句改写成短句。
清洗规则一定要基于自己会议的真实文本做样本统计,不要直接套通用停用词表。我之前用网上找的一份通用词表,结果把会议上频繁出现的“可能”“大概”“部署”给误删了,闹出过尴尬。后来改成自己标注一百条会议文本,总结出这批会议特有的口语词,效果立竿见影。
4.2 利用热词提升专业术语识别率
会议里最常见的问题不是普通话识别不出来,而是人名、产品名、英文缩写被识别成同音字。比如“智能客服”被写成“智能可服”,“CRM”被写成“西艾尔艾姆”。这种情况调模型本身是改不了的,得靠热词。
腾讯云的语音识别支持上传热词表,把会议中高频出现的专有名词加进去,识别时引擎会对这些词做词汇偏置。操作路径是创建一个词汇表,拿到词汇表ID,然后在创建识别任务时把词汇表ID关联进去。我实验下来的效果是,人名和公司产品名的识别率从不到六成直接拉到九成以上,提升非常明显。
热词表不是越大越好。把几百个冷僻词全塞进去,反而可能让引擎对这些词的依赖过强,影响普通词汇的识别。我一般控制在每次会议前维护一份当前项目的高频词清单,二三十个以内,针对性强就够用了。另外,热词表上传后不是立刻生效的,系统需要一点处理时间,所以不要临时到开会前五分钟才传。
4.3 从文本到结构化会议纪要
识别和清洗做完,我们手里有一份“干净的多说话人逐字稿”。但这还不叫会议纪要,顶多叫“可读的转录文本”。真正的纪要要能回答三个问题:讨论了什么、结论是什么、谁在什么时候做什么事。
我在这个项目里用了一套轻量级的规则抽取:先按句子开头的动词把文本分成“讨论类”“决策类”“待办类”。“决定”“确认”“同意”这类词后面的内容归为结论;“下周一前”“尽快”“本周内”这类时间词加上“负责”“跟进”这类动词,组合起来就是待办候选。把这些候选按说话人聚合成一张待办清单,再人工快速确认一遍,一份可用的会议纪要就出来了。
当然这套规则对比较正式的会议效果好,对发散性极强的头脑风暴会漏掉不少信息。我也试过引入大模型来做摘要和待办抽取,效果确实更好,但这里不展开写,因为那会引入额外的成本和技术复杂度。先用规则把基础版本跑起来,后续有条件再升级,是我一贯的做法。
5. 常见问题与实战排错
5.1 识别结果乱码或为空
遇到过几次任务返回success但Result里没文本的情况,排查下来基本都是音频源的问题。最常见的是录音文件本身是空白的、只有一个声道有声音,或者采样率信息和请求里填的对不上。建议在提交前先本地看一眼音频波形,别拿到一个坏文件直接上传。还有一次是音频格式声明为wav但实际是mp3转码的,解析时出现了乱码。后来我统一了一套预处理脚本:先转成PCM/WAV、统一采样率、再提交。
如果Result返回正常但中文全部变成“???”这种占位符,大概率是引擎模型传错了,比如用英文引擎识别中文。检查一下EngineModelType是不是传成了16k_en,这种低级错误排查起来很费时间,但命中率很高。
5.2 长音频超时与并发限制
录音文件识别虽然支持长音频,但也不是无限制的。传一个五小时的录像是能提交,但任务处理时间会非常久,而且一旦中间出错,重试成本很高。我后来给自己定了一个规则:超过两小时的音频先按会议议程切分成几个段落,逐个提交任务,再在后处理时按时间拼接。分段有个额外好处:每个段的识别独立,如果中间某一段失败,只需要重试这一段,不用整份重来。
并发限制同样要留意。同一账号默认的并发任务数是有限的,如果在会议集中时段猛提交几十个任务,会收到并发超限的错误。我的做法是在代码里加一个简单的任务队列,限制同时进行中的任务数,比如最多同时跑5个,剩下的排队等待。
5.3 准确率不达标从哪几个维度排查
如果一段录音识别准确率明显偏低,我建议按顺序排查四个维度:
第一看录音信噪比。背景音乐、空调声、多人抢话都会严重影响ASR,这是最常见的原因。会议现场尽量用全向麦贴近说话人,不要坐在离麦三米远的位置。
第二看口音和语种。带浓重方言或大量英文穿插的会议,通用模型确实吃力。换合适的引擎模型,或通过热词做针对性优化。
第三看专业术语密度。一个发布会级别的会议,满场都是新产品代号,不加热词表识别率一定惨。这个前面说过,热词是性价比最高的优化手段。
第四看音频预处理。音量过低会被引擎当成环境噪声,音量过高会削波产生谐波干扰。如果录音电平不稳定,我建议先做响度归一化,把平均电平调到约负18dBFS左右。
5.4 成本控制与配额监控
云服务按量计费,用得爽不等于可以乱用。我自己踩过的坑是调试阶段不注意,同一个测试音频反复提交了几十次,月底看账单才意识到浪费。后来加了两条纪律:一是调试时用短音频,10秒就够验证链路,不要每次拿完整会议来试;二是代码里统一封装了一个调用的代理层,所有请求先进缓存,如果同样的音频文件已经识别过,直接命中结果,不重复调用。
另外要经常看一眼控制台上的用量统计和余额告警。我还设了一个定时脚本清理过期的转写结果文件,避免对象存储里的临时文件越堆越多。
6. 收个尾:个人体会与后续扩展
这个会议小帮手做下来,我最深的体会是:项目的难点不在“调用一次语音识别”,而在把一次识别结果做成“真的能直接用的会议纪要”。识别、清洗、说话人拆分、待办抽取,每一环节都藏着无数细节。真正有价值的不是那段调用API的代码,而是后面一整套文本加工管线。
最后分享一个小技巧:如果以后想把这套东西做得更智能,可以在文本清洗后把逐字稿交给大模型做摘要和行动项抽取,但一定不要省掉清洗这一步。大模型见到满是“嗯嗯啊啊”的原始转写稿,也会被稀释注意力。先洗出干净、分好说话人的文本,后面不管做摘要、翻译还是知识库检索,质量都会上一个台阶。
这个项目目前在我内部用得很顺手,后续我打算再补一个按参会人维度自动汇总发言时长的面板,看看每次会议到底是谁在“话痨”。如果你也打算做类似的会议自动化工具,建议从最痛的整理纪要环节切入,先跑通一条最小的可用链路,再逐步往上加功能。