1. 从一场两小时的复盘会说起:为什么我要自己搭一个会议小帮手
上个月帮一个做企业培训的朋友整理他们内部复盘会的录音,两小时的会议,四个人轮流发言,中间还有白板讨论和视频演示环节。我用某款免费的转写工具跑了一遍,结果出来的文本惨不忍睹——人名全错、专业术语被识别成同音词、说话人完全分不清谁是谁。手动校对花了将近三个小时,比重新听一遍录音还累。
这件事让我下定决心,用腾讯云 AI 语音识别自己搭一套会议小帮手。核心诉求很明确:把会议录音自动转成结构化的文字记录,能区分说话人,能识别专业术语,最好还能自动提取待办事项。听起来要求不少,但实际拆解下来,腾讯云语音识别(ASR)加上一些后处理逻辑,完全能覆盖。
这篇文章适合两类人看:一类是经常需要整理会议纪要的职场人,想找个靠谱的自动化方案;另一类是有一定开发基础,想了解腾讯云 ASR 实际落地细节的技术同学。我会从选型逻辑、环境准备、核心代码、踩坑记录到进阶优化,把整个搭建过程完整拆一遍。所有代码和配置都是我自己跑通过的,你照着抄作业基本不会翻车。
先说结论:整套方案的核心链路是「录音文件 → 腾讯云 ASR 异步识别 → 说话人分离 → 文本后处理 → 结构化输出」。其中腾讯云 ASR 负责最核心的语音转文字,说话人分离用它的高级版能力,后处理部分我自己写了一些规则和轻量模型来补足。下面逐层展开。
2. 为什么是腾讯云 ASR:选型时我对比了哪些维度
2.1 会议场景对语音识别的四个硬要求
在选型之前,我先把自己对会议转写的需求列了一张清单,按优先级排序:
| 需求维度 | 具体要求 | 优先级 |
|---|---|---|
| 识别准确率 | 中文普通话场景下,安静环境准确率 95% 以上 | 最高 |
| 说话人分离 | 能区分不同发言人,标注每段话是谁说的 | 高 |
| 专业术语 | 支持自定义词表,能识别行业术语和产品名 | 高 |
| 长音频支持 | 单文件支持 2 小时以上,不需要手动切分 | 中 |
| 时间戳 | 每句话有精确的起止时间,方便定位回听 | 中 |
| 成本 | 按量计费,小团队用得起 | 中 |
这六个维度里,前三个是决定性的。识别准确率不行,后面所有后处理都是白搭;说话人分离做不好,会议纪要就没法用;专业术语识别不了,每次都要手动改,自动化就失去了意义。
2.2 腾讯云 ASR 的几个关键能力
腾讯云语音识别在会议场景下有几个能力是我最终选它的原因:
第一,支持异步识别长音频。会议录音动辄一两个小时,实时流式识别虽然延迟低,但需要一直保持连接,对网络稳定性要求高。异步识别是把文件传到对象存储,提交任务后等结果,更适合会议录音这种「录完再转」的场景。单文件支持最长 5 小时,采样率 16k 以上,基本覆盖所有会议场景。
第二,说话人分离能力。腾讯云 ASR 的高级版支持说话人分离(也叫声纹分离),能自动判断音频中有几个说话人,并把每句话归属到对应的说话人。实测下来,2 到 4 人的会议场景,分离准确率相当可用。超过 6 人或者多人同时说话的场景,准确率会下降,这个后面会细说。
第三,热词和自训练能力。你可以上传自定义词表,把公司内部的产品名、项目代号、人名加进去,识别时会优先匹配这些词。这个功能对会议场景太重要了——没有它,「张总」可能被识别成「章总」,「迭代」可能变成「鞋带」。
第四,时间戳粒度细。返回结果里每句话都有开始和结束时间,精确到毫秒。这个对做会议纪要特别有用,你可以点击某句话直接跳转到录音对应位置回听。
2.3 和其他方案的对比
我也试过一些开源的语音识别方案,比如用某开源模型自己部署。优点是数据不出本地,缺点是部署成本高、识别准确率尤其是中文场景下差距明显,而且说话人分离需要额外集成声纹识别模块,工程量翻倍。对于个人开发者和小团队来说,直接用云服务是性价比最高的选择。
腾讯云 ASR 的计费方式是按音频时长计费,具体价格在官网可以查到,这里不展开。对于一个月开几次会的团队来说,成本基本可以忽略不计。
3. 动手之前:环境准备与账号配置的完整清单
3.1 账号开通与密钥管理
第一步是在腾讯云控制台开通语音识别服务。进入控制台后搜索「语音识别」,开通服务。开通后需要在「访问管理」里创建一个子账号或者使用主账号的 API 密钥。这里有个安全建议:不要直接用主账号的密钥,创建一个子账号,只授予语音识别和对象存储的权限,这样即使密钥泄露,影响范围也可控。
拿到 SecretId 和 SecretKey 之后,不要硬编码在代码里。我习惯用环境变量管理:
export TENCENTCLOUD_SECRET_ID="your_secret_id" export TENCENTCLOUD_SECRET_KEY="your_secret_key"如果你用 Python,也可以用配置文件或者密钥管理服务。总之,密钥管理这件事,怎么谨慎都不为过。
3.2 对象存储的准备工作
腾讯云 ASR 的异步识别需要音频文件先上传到对象存储(COS)。所以你需要:
- 创建一个 COS 存储桶,地域选择离你最近的,比如广州或上海。
- 记录下存储桶的名称和所属地域,后面调用 API 时要用。
- 确保你的账号有该存储桶的读写权限。
音频文件上传到 COS 之后,生成一个可访问的 URL(可以是临时签名 URL,也可以是公有读的 URL)。腾讯云 ASR 会从这个 URL 拉取音频文件进行识别。
注意:如果使用临时签名 URL,要确保 URL 的有效期覆盖整个识别过程。异步识别任务提交后,腾讯云后台拉取文件的时间不确定,建议签名有效期至少设置 1 小时以上。
3.3 Python 环境与依赖安装
我用的 Python 3.9,依赖主要是腾讯云官方 SDK:
pip install tencentcloud-sdk-python pip install cos-python-sdk-v5第一个是腾讯云 API 的通用 SDK,第二个是对象存储的 SDK。如果你只需要提交识别任务,其实第一个就够了,但上传文件到 COS 需要第二个。
另外建议装一个pydub用于音频格式转换,有时候会议录音是 m4a 或者 wav 格式,需要统一转成 16k 采样率的 wav 或 mp3:
pip install pydubpydub 依赖 ffmpeg,所以系统里还需要安装 ffmpeg。macOS 上用brew install ffmpeg,Ubuntu 上用apt install ffmpeg。
4. 核心链路拆解:从录音文件到结构化会议纪要
4.1 音频预处理:采样率和格式的统一
腾讯云 ASR 对音频格式有要求,支持的格式包括 wav、mp3、m4a、aac 等,采样率建议 16k 或 8k。会议录音如果是手机录的,可能是 44.1k 采样率的 m4a,需要先转成 16k 的 wav。
from pydub import AudioSegment def preprocess_audio(input_path, output_path): audio = AudioSegment.from_file(input_path) audio = audio.set_frame_rate(16000).set_channels(1) audio.export(output_path, format="wav") return output_path这段代码做了两件事:把采样率统一到 16000Hz,把声道数设为 1(单声道)。为什么要转单声道?因为会议录音通常是多人通过同一个麦克风说话,立体声反而会增加文件大小,对识别准确率没有帮助。单声道文件更小,上传更快。
实操心得:如果你的录音文件超过 100MB,建议先压缩成 mp3 格式再上传。mp3 在 16k 采样率下,一小时的音频大约 30MB 左右,上传速度快很多,识别准确率几乎不受影响。
4.2 上传到对象存储并生成访问链接
音频预处理完成后,上传到 COS:
from qcloud_cos import CosConfig, CosS3Client def upload_to_cos(local_file, bucket, region, cos_path): config = CosConfig( Region=region, SecretId=os.environ["TENCENTCLOUD_SECRET_ID"], SecretKey=os.environ["TENCENTCLOUD_SECRET_KEY"] ) client = CosS3Client(config) with open(local_file, "rb") as f: client.put_object( Bucket=bucket, Body=f, Key=cos_path ) # 生成临时访问 URL url = client.get_presigned_url( Method="GET", Bucket=bucket, Key=cos_path, Expired=3600 ) return url这里生成的临时 URL 有效期是 3600 秒,也就是一小时。对于大多数会议录音来说够用了。如果你的音频特别长,识别任务排队时间可能超过一小时,那就把 Expired 设大一点。
4.3 提交异步识别任务
这是整个链路的核心步骤。腾讯云 ASR 的异步识别接口是CreateRecTask,关键参数包括:
| 参数 | 说明 | 推荐值 |
|---|---|---|
| EngineModelType | 引擎模型类型 | 16k_zh(中文通用) |
| ChannelNum | 声道数 | 1 |
| ResTextFormat | 返回结果格式 | 2(带时间戳和说话人) |
| SpeakerDiarization | 是否开启说话人分离 | 1(开启) |
| SpeakerNumber | 说话人数量 | 0(自动判断) |
| HotwordList | 热词列表 | 自定义 |
from tencentcloud.common import credential from tencentcloud.asr.v20190614 import asr_client, models def submit_asr_task(audio_url): cred = credential.Credential( os.environ["TENCENTCLOUD_SECRET_ID"], os.environ["TENCENTCLOUD_SECRET_KEY"] ) client = asr_client.AsrClient(cred, "ap-guangzhou") req = models.CreateRecTaskRequest() req.EngineModelType = "16k_zh" req.ChannelNum = 1 req.ResTextFormat = 2 req.SpeakerDiarization = 1 req.SpeakerNumber = 0 req.SourceType = 0 req.Url = audio_url req.HotwordList = ["迭代", "复盘", "OKR", "对齐", "闭环"] resp = client.CreateRecTask(req) return resp.Data.TaskId提交成功后返回一个 TaskId,后面用这个 ID 查询识别结果。
4.4 轮询识别结果
异步任务提交后需要轮询查询结果。腾讯云提供了DescribeTaskStatus接口:
import time def poll_asr_result(task_id, interval=10, max_retries=60): cred = credential.Credential( os.environ["TENCENTCLOUD_SECRET_ID"], os.environ["TENCENTCLOUD_SECRET_KEY"] ) client = asr_client.AsrClient(cred, "ap-guangzhou") req = models.DescribeTaskStatusRequest() req.TaskId = task_id for i in range(max_retries): resp = client.DescribeTaskStatus(req) status = resp.Data.Status if status == 2: # 识别完成 return resp.Data.Result elif status == 3: # 识别失败 raise Exception(f"识别失败: {resp.Data.ErrorMsg}") time.sleep(interval) raise TimeoutError("识别超时")Status 的含义:0 是等待中,1 是识别中,2 是识别完成,3 是识别失败。轮询间隔建议 10 秒,太频繁没必要,太慢又浪费时间。两小时的音频,识别时间大约在 3 到 5 分钟。
4.5 解析返回结果
识别完成后返回的 Result 是一个 JSON 字符串,结构大致如下:
{ "Result": "...", "SpeakerDiarization": 1, "SentenceList": [ { "SpeakerId": 0, "StartTime": 0, "EndTime": 3200, "Text": "我们先过一下上周的迭代进度" }, { "SpeakerId": 1, "StartTime": 3200, "EndTime": 6800, "Text": "好的,我这边有三个需求已经上线了" } ] }SentenceList 里每句话都有 SpeakerId、StartTime、EndTime 和 Text。SpeakerId 是说话人的编号,从 0 开始。这个结构已经非常接近会议纪要的格式了,只需要做一点后处理就能输出可读性很强的文档。
5. 踩坑记录:那些文档里没写的实际问题
5.1 说话人分离在多人重叠发言时的表现
这是我在实际使用中遇到的最大问题。腾讯云 ASR 的说话人分离在两人交替发言的场景下表现很好,准确率目测在 90% 以上。但一旦出现多人同时说话,比如讨论激烈时几个人抢着发言,分离准确率会明显下降。
具体表现是:重叠部分的语音会被归到其中一个说话人名下,或者直接丢失。我试过一段四人讨论的录音,其中有大约 30 秒的激烈争论,转写结果里这段内容几乎不可读。
应对策略:在会议录音时尽量使用指向性麦克风,减少环境噪音和重叠拾音。如果条件允许,让每个人用单独的麦克风录音,后期分轨识别,准确率会高很多。但这对会议硬件有要求,不是所有场景都能满足。
另一个折中方案是在后处理阶段做人工校对。我会把识别结果按说话人分组,标注出置信度较低的段落,提醒用户重点校对。腾讯云 ASR 的返回结果里虽然没有直接的置信度字段,但可以通过句子长度、语速等特征间接判断。
5.2 热词列表的配置技巧
热词功能很实用,但配置有讲究。我一开始把公司所有产品名、项目代号、人名一股脑全加进去了,大概有两百多个词。结果发现识别准确率反而下降了——因为热词太多,引擎在匹配时产生了干扰。
后来我做了减法,只保留最近一个月会议中高频出现的词,控制在 50 个以内。准确率明显回升。
实操心得:热词列表不是越多越好。建议按项目或按会议主题分组维护,每次识别时只加载相关的热词组。比如产品评审会加载产品词表,技术方案会加载技术词表。另外,热词的长度建议在 2 到 6 个字之间,太短的词容易误匹配,太长的词引擎可能不支持。
5.3 长音频的断句问题
两小时的会议录音,识别结果是一大段连续的文本。腾讯云 ASR 会按静音段自动断句,但会议场景下静音段往往不明显——有人说话时背景可能有键盘声、翻页声,导致断句不准确。
我的处理方式是在后处理阶段做二次断句。规则很简单:如果一句话超过 50 个字,就在标点符号处强制断开;如果两句话之间的时间间隔超过 2 秒,就认为是两个独立的段落。
def refine_sentences(sentence_list, max_len=50, gap_threshold=2000): refined = [] for sent in sentence_list: text = sent["Text"] if len(text) > max_len: # 按标点断句 parts = split_by_punctuation(text, max_len) for part in parts: refined.append({**sent, "Text": part}) else: refined.append(sent) # 合并时间间隔过大的句子为不同段落 paragraphs = [] current_para = [] for i, sent in enumerate(refined): if i > 0 and sent["StartTime"] - refined[i-1]["EndTime"] > gap_threshold: paragraphs.append(current_para) current_para = [] current_para.append(sent) if current_para: paragraphs.append(current_para) return paragraphs这段代码做了两件事:把过长的句子按标点断开,把时间间隔大的句子分成不同段落。实测下来,输出的文本可读性提升明显。
5.4 识别失败的重试策略
异步识别任务偶尔会失败,原因可能是文件格式问题、网络超时、或者服务端临时故障。我的做法是加一个重试机制:失败后等待 30 秒重新提交,最多重试 3 次。如果三次都失败,就把任务标记为需要人工处理,并记录失败原因。
另外要注意,如果音频文件本身有问题(比如采样率不对、文件损坏),重试再多次也没用。所以在提交任务之前,最好先做一次音频校验,确保格式和采样率符合要求。
6. 让会议纪要真正可用:后处理与结构化输出
6.1 说话人标签的映射
识别结果里的 SpeakerId 是 0、1、2 这样的数字,直接输出没人看得懂。需要做一个映射,把数字映射成真实姓名。这个映射可以手动配置,也可以基于声纹注册自动匹配。
手动配置的方式很简单,维护一个字典:
speaker_map = { 0: "张工", 1: "李经理", 2: "王产品", 3: "赵设计" }但问题是,每次会议的第一个说话人不一定是同一个人。所以更可靠的方式是在会议开始时让每个人说一句话做声纹注册,后面识别时自动匹配。腾讯云 ASR 本身不提供声纹注册功能,但可以结合其他声纹识别服务来实现。如果不想搞这么复杂,就在输出文档里保留 SpeakerId,让用户自己对照。
6.2 待办事项的自动提取
会议纪要最有价值的部分是待办事项。我写了一个简单的规则引擎来提取:
import re def extract_action_items(sentences): patterns = [ r"(.+?)负责(.+?)[,。]", r"(.+?)跟进(.+?)[,。]", r"(.+?)在(.+?)之前完成(.+?)[,。]", r"下周(.+?)之前(.+?)[,。]" ] action_items = [] for sent in sentences: for pattern in patterns: match = re.search(pattern, sent["Text"]) if match: action_items.append({ "speaker": sent["SpeakerId"], "time": sent["StartTime"], "content": sent["Text"], "groups": match.groups() }) return action_items这个规则引擎覆盖了常见的待办表达方式,比如「某某负责某某事」「某某跟进某某」「下周之前完成某某」。实测下来,能提取出 70% 左右的待办事项。剩下的 30% 需要人工补充,但已经大大减少了整理工作量。
6.3 输出格式的选择
最终输出我提供了三种格式:
Markdown 格式:适合直接粘贴到文档工具里,结构清晰,支持标题、列表、加粗等格式。
JSON 格式:适合程序进一步处理,比如导入到项目管理工具或者数据库。
纯文本格式:适合快速浏览和搜索。
Markdown 格式的输出示例:
## 会议纪要 **会议时间**:2024-01-15 14:00-16:00 **参会人员**:张工、李经理、王产品、赵设计 ### 1. 上周迭代进度回顾 **张工**(00:00:00 - 00:00:32): 我们先过一下上周的迭代进度。 **李经理**(00:00:32 - 00:01:08): 好的,我这边有三个需求已经上线了,分别是用户中心改版、订单流程优化和消息推送重构。 ### 2. 待办事项 - 李经理负责整理用户中心改版的测试报告 - 王产品跟进订单流程优化的用户反馈 - 赵设计在下周三之前完成消息推送的视觉稿这种格式的会议纪要,直接发给参会人员就能用,不需要再做二次整理。
7. 进阶优化:让识别准确率再上一个台阶
7.1 音频降噪的前置处理
如果会议录音环境比较嘈杂,可以在上传之前做一次降噪处理。我试过用noisereduce这个 Python 库,效果还不错:
import noisereduce as nr import soundfile as sf def denoise_audio(input_path, output_path): data, rate = sf.read(input_path) reduced_noise = nr.reduce_noise(y=data, sr=rate) sf.write(output_path, reduced_noise, rate)降噪之后,识别准确率在嘈杂环境下能提升 5 到 10 个百分点。但要注意,降噪过度会导致语音失真,反而影响识别。建议先在小段音频上测试,找到合适的降噪强度。
7.2 自定义语言模型的训练
腾讯云 ASR 支持自训练模型,你可以上传自己的语料,训练一个针对特定领域的识别模型。比如你们公司有大量内部术语和缩写,通用模型识别不了,就可以用自训练模型来解决。
自训练模型的流程是:准备语料(文本+对应音频)→ 上传到腾讯云 → 训练模型 → 在识别时指定模型 ID。训练时间大约需要几个小时到一天,具体取决于语料量。对于有长期会议转写需求的团队来说,这个投入是值得的。
7.3 实时识别的补充方案
异步识别适合会后整理,但有些场景需要实时字幕,比如远程会议。腾讯云 ASR 也提供实时流式识别接口,可以做到边说边转。实时识别的延迟在 1 到 2 秒左右,准确率略低于异步识别,但胜在即时性。
我的做法是两者结合:远程会议用实时识别做字幕,同时录音,会后用异步识别做一份更准确的纪要。这样既满足了实时需求,又保证了最终文档的质量。
7.4 成本控制的几个技巧
腾讯云 ASR 按音频时长计费,会议多的时候成本会累积。几个控制成本的技巧:
- 音频压缩:把 wav 转成 mp3,文件大小减少 70% 以上,识别时长不变,但上传流量费省了。
- 静音裁剪:会议录音里往往有大量静音段,可以用工具自动裁剪掉,减少识别时长。
- 按需识别:不是所有会议都需要转写,只对重要的会议开启识别。
- 监控用量:在控制台设置用量告警,避免意外超支。
8. 我在这套方案上踩过的三个坑和对应的解法
第一个坑是音频采样率不匹配。我一开始直接上传手机录的 m4a 文件,采样率是 44.1k,识别结果里有很多莫名其妙的错误。后来统一转成 16k 采样率,准确率立刻上来了。腾讯云 ASR 虽然支持多种采样率,但 16k 是针对语音识别优化的,效果最好。
第二个坑是热词列表的维护。我一开始把热词写死在代码里,后来发现每次加词都要改代码、重新部署,太麻烦。后来改成从配置文件读取,每次识别时动态加载。配置文件用 YAML 格式,按项目分组,维护起来方便很多。
第三个坑是轮询超时。有一次识别一个三小时的会议录音,轮询了十分钟还没结果,我以为出问题了,就重新提交了一次任务。结果两个任务都完成了,白白多花了一份钱。后来我把轮询的超时时间设成了 30 分钟,并且加了日志记录,避免重复提交。
这套方案我现在已经稳定用了几个月,每周处理三到五场会议录音,平均每场会议节省一个小时的整理时间。识别准确率在安静环境下能达到 95% 以上,嘈杂环境下也有 85% 左右。说话人分离在两人对话场景下几乎不需要手动修正,多人讨论场景下需要少量校对。
如果你也想搭一套类似的会议小帮手,建议先从最简单的链路跑通——上传音频、提交识别、输出文本。跑通之后再逐步加入说话人分离、热词、后处理这些进阶功能。不要一上来就追求完美,先让流程跑起来,再慢慢优化。