1. 会议记录这件事,到底难在哪
开会这件事,估计每个打工人都有一肚子苦水。尤其是那种一开就是两小时的跨部门对齐会,开完之后脑子嗡嗡的,还得硬着头皮整理会议纪要。我以前试过一边开会一边疯狂打字,结果发现根本跟不上——别人语速稍微快一点,我这边就开始丢字漏句,最后整理出来的东西自己都看不懂。后来换成先录音、会后慢慢听,结果更崩溃:两小时的录音,我得花三四个小时去回放、暂停、打字,效率低到令人发指。
这个项目的出发点特别朴素:能不能让机器帮我把会议录音自动转成文字,我只需要在文字基础上做润色和提炼就行。说白了,就是找一个靠谱的语音识别服务,把会议录音丢进去,拿回一份带时间戳的文字稿。听起来简单,但真做起来,里面有不少门道。
我选择腾讯云 AI 语音识别来做这件事,核心原因是它在中文场景下的识别准确率比较稳,尤其是对会议这种多人交替说话、偶尔有专业术语的场景,支持得比较好。而且它提供了完整的 API 接口,可以自己写脚本批量处理,不用手动一个个上传。这套方案适合谁呢?我觉得三类人最需要:一是经常要整理会议纪要的行政或项目管理人员,二是做用户访谈、需求调研的产品同学,三是像我这样懒得记笔记、但又不想漏掉关键信息的普通开发者。
整篇文章我会从方案设计、核心接口调用、实操步骤、参数调优、常见问题排查这几个角度,把整个“会议小帮手”的搭建过程拆开讲清楚。你不需要是 AI 专家,只要会一点 Python 基础,就能跟着做出来。
2. 整体方案设计与技术选型思路
2.1 为什么选语音识别而不是语音转文字工具
市面上语音转文字的工具其实不少,有在线的、有本地的、有免费的、有按量付费的。我一开始也试过一些免费的在线工具,但很快就放弃了。原因有三个:第一,免费工具通常有时长限制,会议录音动辄一两个小时,根本传不上去;第二,免费工具的识别准确率参差不齐,尤其是遇到多人对话、有口音、有背景噪音的情况,错字连篇,后期修改的成本比重新听一遍还高;第三,很多工具不支持批量处理,我有十几个录音文件要处理,一个个手动上传下载,光操作就累死了。
腾讯云 AI 语音识别的优势在于:它提供了一整套 API,包括一句话识别、录音文件识别、实时语音识别等不同场景的接口。对于会议录音这种“先录后转”的需求,最合适的是录音文件识别接口。这个接口支持上传音频文件,异步返回识别结果,支持长达数小时的音频,而且可以设置热词、说话人分离等高级功能。说白了,就是你把文件丢过去,等几分钟,拿回一份结构化的文字稿。
2.2 整体架构:从录音到文字稿的完整链路
整个“会议小帮手”的架构其实不复杂,我画不出图,但可以用文字描述清楚。核心链路是这样的:
- 录音采集:用手机或电脑录制会议音频,保存为常见格式(如 mp3、wav、m4a)。
- 文件上传:把音频文件上传到对象存储服务,或者直接通过 API 上传。
- 提交识别任务:调用录音文件识别接口,提交音频文件的 URL 或本地文件,设置识别参数。
- 轮询结果:因为识别是异步的,需要定时查询任务状态,直到识别完成。
- 获取文字稿:识别完成后,拉取结果,得到带时间戳和说话人标记的文字内容。
- 后处理:对文字稿进行格式化、分段、关键词提取等处理,生成可读性强的会议纪要。
这个链路里,最核心的是第 3 步和第 5 步,也就是提交任务和获取结果。腾讯云的录音文件识别接口设计得比较合理,提交任务后返回一个 TaskId,后续用这个 TaskId 去查询结果。整个流程不需要你一直守着,可以批量提交多个任务,然后统一轮询。
2.3 技术选型:为什么用 Python 而不是其他语言
我选 Python 来做这个工具,理由很直接:腾讯云的 SDK 对 Python 支持最好,文档最全,示例代码最多。而且 Python 在处理文件、调用 API、做文本后处理方面都非常方便。如果你习惯用 Java 或 Go,腾讯云也有对应的 SDK,但 Python 的生态更丰富,遇到问题更容易找到解决方案。
另外,Python 的requests库和json库可以很方便地处理 HTTP 请求和 JSON 数据,即使不用 SDK,直接调 REST API 也不难。我个人的习惯是:先用 SDK 快速跑通,如果遇到性能瓶颈或特殊需求,再考虑直接调 API。
2.4 成本估算:到底要花多少钱
说到钱,这是很多人关心的问题。腾讯云 AI 语音识别的录音文件识别接口是按音频时长计费的,具体价格会根据不同的套餐和活动有所浮动。我大概算了一下:如果每天处理 2 小时的会议录音,一个月按 22 个工作日算,总共 44 小时。按照常见的阶梯计价,每个月的成本大概在几十块钱左右。相比你花几个小时手动整理纪要的时间成本,这个投入是非常划算的。
当然,如果你只是偶尔用一下,腾讯云通常也会提供一定的免费额度,足够你测试和小规模使用。我的建议是:先跑通流程,确认效果满意后,再根据实际使用量选择合适的套餐。
3. 核心接口调用与参数详解
3.1 录音文件识别接口的核心参数
腾讯云录音文件识别接口的核心参数不多,但每一个都很关键。我挑几个最重要的来说:
- EngineModelType:引擎模型类型。这个参数决定了用哪种识别模型。对于会议场景,我推荐用
16k_zh(16k 采样率中文通用模型)。如果你的会议有较多英文内容,可以考虑16k_zh_en(中英文混合模型)。实测下来,16k_zh对纯中文会议的识别准确率最高。 - ChannelNum:声道数。通常会议录音是单声道,填
1。如果是双声道录音(比如左右声道分别录不同的人),可以填2,但需要确认录音设备是否支持。 - ResTextFormat:返回结果格式。这个参数控制返回的文字稿格式。我一般填
0,表示返回带时间戳的详细结果。如果你只需要纯文本,可以填1,但那样会丢失时间信息,不利于后续定位。 - SpeakerDiarization:说话人分离。这个参数非常有用,开启后可以自动区分不同的说话人。对于会议场景,我强烈建议开启,填
true。这样返回的结果里会带上SpeakerId,方便你知道哪句话是谁说的。 - HotwordList:热词列表。这个参数可以传入一些专业术语、人名、产品名等,提高这些词的识别准确率。比如你们公司内部有个产品叫“星云平台”,默认模型可能会识别成“幸运平台”,加上热词后就能纠正过来。
这些参数在提交任务时通过 JSON 格式传入,具体格式可以参考官方文档。我下面会给出一个完整的示例。
3.2 提交识别任务的完整代码
先确保你安装了腾讯云的 Python SDK:
pip install tencentcloud-sdk-python然后,提交任务的代码大概长这样:
import json 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 # 密钥信息,建议从环境变量读取,不要硬编码 secret_id = "你的SecretId" secret_key = "你的SecretKey" cred = credential.Credential(secret_id, secret_key) http_profile = HttpProfile() http_profile.endpoint = "asr.tencentcloudapi.com" client_profile = ClientProfile() client_profile.httpProfile = http_profile client = asr_client.AsrClient(cred, "ap-guangzhou", client_profile) # 提交任务 req = models.CreateRecTaskRequest() params = { "EngineModelType": "16k_zh", "ChannelNum": 1, "ResTextFormat": 0, "SourceType": 0, "Url": "https://your-bucket.cos.ap-guangzhou.myqcloud.com/meeting.mp3", "SpeakerDiarization": True, "HotwordList": "星云平台|10,项目对齐|5" } req.from_json_string(json.dumps(params)) resp = client.CreateRecTask(req) print(resp.to_json_string())这段代码里,SourceType填0表示通过 URL 提交音频文件,填1表示直接上传音频数据。我一般用0,因为先把文件传到对象存储更稳定,也方便管理。Url就是音频文件的公网可访问地址。
提交成功后,你会得到一个TaskId,这个 ID 是后续查询结果的唯一凭证,一定要保存好。
3.3 查询识别结果的正确姿势
提交任务后,识别是异步进行的。你需要定时查询任务状态。查询的代码大概是这样:
req = models.DescribeTaskStatusRequest() params = { "TaskId": 你的TaskId } req.from_json_string(json.dumps(params)) resp = client.DescribeTaskStatus(req) print(resp.to_json_string())返回结果里有一个Status字段:0表示任务成功,1表示任务失败,2表示任务进行中,3表示任务未开始。你需要轮询这个接口,直到Status变成0或1。
轮询的频率建议不要太快,我一般设置成每 10 秒查一次。如果音频比较长,可能需要等几分钟。你可以写一个循环,加上超时机制,避免无限等待。
3.4 解析返回的文字稿
当Status变成0后,返回结果里会包含Result字段,里面是识别出的文字内容。如果开启了说话人分离,结果里还会包含SpeakerId。我一般会把结果解析成这样的结构:
result = json.loads(resp.Result) for item in result["ResultDetail"]: speaker = item.get("SpeakerId", "未知") text = item["FinalSentence"] start_time = item["StartMs"] end_time = item["EndMs"] print(f"[{speaker}] {start_time}ms - {end_time}ms: {text}")这样你就能得到一份带说话人和时间戳的文字稿。后续可以根据时间戳做分段,根据说话人做角色标注,最终生成一份可读性强的会议纪要。
4. 实操全流程:从录音到纪要的每一步
4.1 录音采集的注意事项
录音质量直接决定了识别准确率。我踩过的坑包括:录音设备离说话人太远,导致声音太小;会议室空调噪音太大,导致背景噪声干扰;多人同时说话,导致识别结果混乱。后来我总结了几条经验:
- 尽量用专业的会议麦克风,比如全向麦克风,放在桌子中间,能均匀采集所有人的声音。
- 录音格式选 mp3 或 wav,采样率至少 16k,比特率不要太低。我一般用 16k 采样率、16bit 位深的 wav 格式,识别效果最好。
- 会前提醒大家尽量轮流发言,避免多人同时说话。虽然说话人分离功能能区分不同人,但如果两个人同时说话,识别结果会混在一起,很难拆分。
- 如果条件允许,可以每个人单独录音,但这样后期需要做多轨对齐,复杂度较高。我一般还是用单轨录音,靠说话人分离来区分。
4.2 音频文件上传与管理的实操
我习惯把音频文件先上传到对象存储,然后用公网 URL 提交识别任务。这样做的好处是:文件管理方便,可以批量上传;识别任务提交后,即使本地文件删了也不影响;而且对象存储通常有 CDN 加速,上传和下载速度都很快。
上传的步骤很简单:登录对象存储控制台,创建一个存储桶,把音频文件拖进去,然后获取文件的公网访问地址。注意要设置存储桶的权限为“公有读私有写”,否则腾讯云的识别服务无法访问你的文件。
如果你不想用对象存储,也可以直接用SourceType=1上传音频数据。但这种方式需要你把音频文件读成二进制,然后 Base64 编码后传给接口,代码会复杂一些,而且大文件传输容易超时。我建议还是用对象存储的方式。
4.3 批量提交任务的脚本设计
如果你有多个会议录音要处理,一个个手动提交太慢了。我写了一个批量提交的脚本,核心逻辑是:遍历指定目录下的所有音频文件,依次上传到对象存储,然后提交识别任务,把返回的 TaskId 和文件名对应关系保存到一个 JSON 文件里。
import os import json from tencentcloud.common import credential from tencentcloud.asr.v20190614 import asr_client, models def batch_submit(audio_dir, output_file): tasks = {} for filename in os.listdir(audio_dir): if not filename.endswith((".mp3", ".wav", ".m4a")): continue # 假设文件已经上传到对象存储,这里直接构造 URL url = f"https://your-bucket.cos.ap-guangzhou.myqcloud.com/{filename}" task_id = submit_task(url) tasks[filename] = task_id print(f"已提交: {filename}, TaskId: {task_id}") with open(output_file, "w") as f: json.dump(tasks, f, ensure_ascii=False, indent=2)这个脚本跑完之后,你会得到一个tasks.json文件,里面记录了每个文件对应的 TaskId。后续查询结果时,直接读这个文件就行。
4.4 结果轮询与超时处理
批量提交后,你需要轮询所有任务的状态。我写了一个轮询脚本,逻辑是:读取tasks.json,遍历每个 TaskId,查询状态。如果状态是“进行中”,就等一段时间再查;如果状态是“成功”,就拉取结果并保存;如果状态是“失败”,就记录错误信息。
import time import json from tencentcloud.asr.v20190614 import asr_client, models def poll_results(tasks_file, output_dir, max_wait=3600): with open(tasks_file, "r") as f: tasks = json.load(f) pending = set(tasks.keys()) start_time = time.time() while pending and (time.time() - start_time) < max_wait: for filename in list(pending): task_id = tasks[filename] status = query_status(task_id) if status == 0: result = get_result(task_id) save_result(output_dir, filename, result) pending.remove(filename) print(f"已完成: {filename}") elif status == 1: print(f"失败: {filename}") pending.remove(filename) if pending: time.sleep(10) if pending: print(f"超时未完成: {pending}")这个脚本里,max_wait是最大等待时间,我一般设置成 3600 秒(1 小时)。如果超过这个时间还有任务没完成,就记录下来,后续手动处理。
4.5 文字稿后处理:从原始结果到可读纪要
拿到原始文字稿后,还需要做一些后处理,才能变成可读性强的会议纪要。我一般做这几件事:
- 按时间戳分段:如果两句话之间的时间间隔超过 2 秒,就认为是一个新的段落。
- 按说话人合并:把同一个说话人的连续发言合并成一段,加上说话人标签。
- 关键词提取:用简单的词频统计或 TF-IDF 算法,提取出现频率较高的关键词,放在纪要开头作为摘要。
- 格式化输出:最终输出成 Markdown 格式,方便复制到笔记软件里。
后处理的代码可以根据你的需求灵活调整。我一般会写一个post_process.py脚本,输入原始 JSON,输出 Markdown 文件。
5. 参数调优与识别准确率提升技巧
5.1 热词列表的配置策略
热词列表是提升识别准确率最直接的手段。我的经验是:热词不要太多,控制在 50 个以内,每个热词的权重设置在 5 到 10 之间。权重太高会导致模型过度偏向热词,反而影响其他词的识别;权重太低则起不到纠正作用。
热词的来源可以是:公司内部的产品名、项目代号、常用缩写、人名等。比如你们团队有个项目叫“天枢”,默认模型可能会识别成“天数”或“天书”,加上热词后就能纠正。另外,热词之间用竖线分隔,权重用冒号分隔,格式是热词:权重。
5.2 说话人分离的适用场景与限制
说话人分离功能在会议场景下非常有用,但它也有一些限制。首先,它需要音频中有明显的说话人切换,如果两个人声音很像,或者说话重叠严重,分离效果会打折扣。其次,说话人分离的准确率受录音质量影响很大,背景噪音越大,分离效果越差。
我的建议是:如果会议人数不多(3 到 5 人),且大家轮流发言,说话人分离的效果通常不错。如果会议人数很多(超过 8 人),或者讨论非常激烈,分离效果可能不太理想,这时候可以考虑手动标注说话人,或者只关注文字内容,不纠结于谁说的。
5.3 音频预处理:降噪与格式转换
如果录音质量不太好,可以在提交识别前做一些预处理。我常用的工具是ffmpeg,可以做降噪、格式转换、采样率调整等操作。比如,把音频统一转成 16k 采样率的 wav 格式:
ffmpeg -i input.mp3 -ar 16000 -ac 1 -c:a pcm_s16le output.wav这个命令把输入文件转成 16k 采样率、单声道、16bit 位深的 wav 格式。实测下来,这种格式的识别效果最好。如果录音有背景噪音,可以用ffmpeg的降噪滤镜:
ffmpeg -i input.wav -af "afftdn=nf=-25" output_denoised.wav这个命令会应用 FFT 降噪,nf=-25是降噪强度,可以根据实际情况调整。注意不要降噪过度,否则会损失人声细节,反而影响识别。
5.4 识别结果的人工校对要点
即使识别准确率很高,也难免有一些错误。我一般会重点校对这几类内容:人名、数字、专业术语、否定词。人名和数字一旦识别错,后续很难发现;专业术语识别错会影响理解;否定词识别错会导致意思完全相反,比如“同意”识别成“不同意”,那就麻烦大了。
校对的时候,我建议对照原始录音,重点听那些识别结果不太通顺的地方。如果时间有限,至少要把关键决策和行动项校对一遍。
6. 常见问题排查与避坑指南
6.1 任务提交失败的原因与排查
提交任务失败是最常见的问题,原因通常有这几类:
| 错误现象 | 可能原因 | 排查方法 |
|---|---|---|
| 返回鉴权失败 | SecretId 或 SecretKey 错误 | 检查密钥是否正确,是否过期 |
| 返回参数错误 | 参数格式不对或缺少必填项 | 对照文档检查参数,确保 JSON 格式正确 |
| 返回文件无法访问 | 音频 URL 不可访问 | 用浏览器打开 URL,确认能正常下载 |
| 返回音频格式不支持 | 音频格式或编码不兼容 | 用 ffmpeg 转成标准 wav 格式后重试 |
| 返回任务数超限 | 并发任务数超过配额 | 减少并发提交数量,或申请提高配额 |
我遇到最多的是“文件无法访问”,通常是因为对象存储的权限设置不对,或者 URL 过期了。解决方法是:确保存储桶权限为公有读,或者使用临时签名 URL。
6.2 识别结果不准确的常见原因
识别结果不准确,通常不是单一原因造成的。我总结了几种常见情况:
- 音频质量差:背景噪音大、说话人声音小、有回声。解决办法是改善录音环境,或用 ffmpeg 做降噪。
- 说话人重叠:多人同时说话,导致识别结果混乱。解决办法是提醒大家轮流发言,或使用说话人分离功能。
- 专业术语多:模型对某些专业术语不熟悉。解决办法是配置热词列表。
- 口音较重:模型对口音识别能力有限。解决办法是选择适合的引擎模型,或提供更多训练数据(但腾讯云不支持自定义训练,只能通过热词优化)。
6.3 轮询超时与任务卡住怎么办
有时候任务会卡在“进行中”状态很久,甚至超过预期时间。这种情况通常是因为音频文件太大,或者服务端负载较高。我的处理方法是:设置一个合理的超时时间(比如 1 小时),如果超时还没完成,就重新提交任务。重新提交时,可以尝试把音频切成几段,分别提交,这样每段处理更快,也不容易卡住。
另外,如果任务状态一直是“未开始”,可能是提交时参数有问题,或者服务端队列满了。这时候可以检查一下参数,或者稍后再试。
6.4 成本控制的几个实用技巧
虽然整体成本不高,但如果你用量大,还是有一些省钱技巧的:
- 音频压缩:在保证识别效果的前提下,尽量压缩音频文件大小。比如把 wav 转成 mp3,比特率降到 64kbps,文件大小能减少一半以上,识别费用也会相应降低。
- 批量提交:把多个短音频合并成一个长音频,一次性提交,减少任务数量。但注意不要合并不同会议的音频,否则结果会混在一起。
- 合理设置超时:避免因为轮询超时导致重复提交,浪费费用。
- 监控用量:定期查看账单和用量统计,及时发现异常消耗。
7. 我的实操心得与后续扩展思路
这套“会议小帮手”我用了大半年,整体感受是:它不能完全替代人工整理,但能节省 70% 以上的时间。以前整理两小时的会议纪要要花三四个小时,现在只需要半小时左右,主要时间花在校对和润色上。识别准确率方面,在录音质量好的情况下,中文会议的准确率能到 90% 以上,加上热词优化后,关键术语的准确率能到 95% 左右。
踩过的坑也不少。最开始我没开说话人分离,结果整理出来的文字稿分不清谁说的,后来开了之后好很多。还有一次热词权重设得太高,导致一些常用词被错误替换,后来把权重降到 5 就正常了。另外,轮询超时的问题也遇到过几次,后来加了重试机制就稳定了。
后续我还想扩展几个功能:一是自动提取行动项,从文字稿里识别出“谁负责什么、什么时候完成”这类信息;二是自动生成会议摘要,用大模型对文字稿做摘要,进一步减少人工整理的工作量;三是和日历集成,会议结束后自动触发识别流程,真正做到“开完会就有纪要”。这些扩展目前还在摸索阶段,等跑通了再分享。
如果你也想搭一套类似的工具,我的建议是:先从最简单的单文件识别跑通,确认效果满意后,再逐步加上批量处理、说话人分离、热词优化等功能。不要一上来就追求大而全,先把核心流程跑顺,后面再慢慢迭代。