ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

用腾讯云AI语音识别,自动生成会议纪要与待办事项

用腾讯云AI语音识别,自动生成会议纪要与待办事项

开完会一小时的录音,真到写纪要的时候,脑子里只剩“刚才说了一堆,具体谁负责什么来着”这种模糊印象。这种场景我经历过太多次,所以干脆把腾讯云 AI 语音识别接到自己的流程里,做了一个能自动出转写、能分说话人、能提取待办事项的会议小帮手。

这篇文章会从需求说起,把整体方案怎么设计、接口怎么调、参数怎么定、识别率怎么调优、以及我实际踩过的坑全部讲清楚。适合手里有真实会议录音场景的运营、市场、产品同学,也适合想快速给内部工具加一个语音转写能力的开发者。不写泛泛的API文档,直接以一套可以落地的“录音文件转写+纪要生成”方案为主线。

1. 从想清楚“会议小帮手”要解决什么开始

1.1 会议场景里真正让人头疼的三件事

会议数据至少有三种形态:大家嘴里说的话、共享屏幕上展示的信息、以及最终要落到执行层的行动项。第一种形态的信息损耗最严重。嘴上说的是“下周排期要提前,小王你跟进一下”,到纪要里往往就变成“下周排期要提前”,至于“谁跟进”这个关键信息,经常直接消失。

做成会议小帮手,第一步不是选接口,而是把需要解决的问题拆开。我按照自己的使用体验,归纳成三个核心诉求:

  • 把语音变成文字,而且要分得出是谁在说话
  • 从大段口语化转写稿里提炼出纪要级信息
  • 把“待办”“负责人”“时间节点”这类信息结构化提取出来

这三个诉求对应着完全不同的技术动作。语音转写靠的是ASR引擎,分说话人靠的是声纹特征,纪要提炼和信息结构化靠的则是后处理规则或大模型。如果一开始就把所有目标寄托在某一个API上,后面很容易被需求带偏。

1.2 为什么用云API而不是本地语音识别方案

我在做这个小帮手之前,先花了一个周末尝试本地语音识别方案。那条路不是说走不通,而是维护成本高得离谱。中文识别效果要追平云端商用接口,需要下载几个GB的模型,还要配好GPU推理环境,而一旦遇到会议室这种多人混音场景,识别率会直线下降。

云API在会议室场景有几个很实在的优势。首先,中文普通话识别效果经过大量真实语料打磨,对会议中常见的口语、语速变化、专业词汇处理得更稳。其次,云接口天然支持长音频异步转写,不用自己处理分段和拼接。最后,按量付费的模式对低频自用场景非常友好,我一个月可能就识别十几个小时的会议录音,成本完全在一个可接受的范围内。

1.3 这个“小帮手”最终长什么样

先描述一下最终效果,大家更容易理解后面的技术选型。我的流程是:会议结束后把录音文件丢进一个脚本,脚本上传音频并创建转写任务,等待识别完成后自动拉取带说话人标记和时间戳的转写文本,然后通过一套规则把文本切成段落、去掉语气词、抽出含有关键动作词的句子,最后生成一个包含“背景、结论、行动项、负责人”的Markdown会议纪要,同步到团队的文档空间。

整个过程从丢入录音到拿到纪要,大约需要几分钟到十几分钟,取决于音频长度和队列排队情况。后面所有技术细节,都是在为这个目标服务。

2. 整体方案设计:从录音采集到纪要输出

2.1 音频采集端决定了一半的识别率

我刚开始测试时犯过一个典型错误:随手用手机录了段会议音频,格式是微信语音转出来的MP3,码率低、人声混杂,结果识别出来的文字基本没法看。后来才想明白,ASR引擎再强,输入音频的质量不行,输出就一定不行。

会议录音采集有几个硬性指标需要明确:

  • 采样率:建议16kHz或以上。语音识别引擎对采样率有明确要求,低于8kHz会损失大量高频信息,中文的声母、韵母识别会明显变差
  • 声道:单声道。双声道如果两个人离麦克风距离不同,反而会造成能量不均
  • 编码格式:WAV/PCM是首选,无损压缩。MP3在低码率下会丢失细节,高码率的M4A/AAC也能接受
  • 录制距离:麦克风离说话人越近越好,多人会议建议使用全向麦克风或专用会议麦

如果手里已经有一份不太合格的录音,可以用FFmpeg做一次简单的预处理,把格式统一成16k单声道WAV。这条命令我基本每次都会用到:

ffmpeg -i input.m4a -ar 16000 -ac 1 -f wav output.wav

参数解释:

  • -ar 16000:重采样到16kHz
  • -ac 1:强制转成单声道
  • -f wav:输出WAV容器格式

有人会问,为什么不直接用48kHz音频提交?很多云端ASR接口会限制采样率范围,超过上限反而要做二次转码,与其让它转,不如自己在本地用FFmpeg统一处理,还能顺便把音量标准化做了。

2.2 选哪个识别产品:实时还是录音文件识别

腾讯云语音识别在会议场景里主要涉及两条产品线,选型的核心判断依据是“音频是不是已经存在”。

  • 录音文件识别(异步任务式):适合会议已经结束、手里有完整录音文件的场景。提交音频URL或base64后返回一个TaskId,轮询状态拉取结果。支持长音频,还能选说话人分离、词级时间戳等能力
  • 实时语音识别(流式):适合会中场景,比如现场大屏显示字幕、实时关键词提醒。通过WebSocket或长连接持续上传语音流,返回实时转写片段

对于我自己的会议小帮手,主线走的是录音文件识别,因为实际处理的是“会议结束后”的场景,不追求实时性。如果后续想做成会中工具,再扩展实时识别也不迟,因为两者的结果格式和下游处理逻辑是可以共用的。

表里的对比可以帮你快速判断该用哪条路:

对比维度录音文件识别实时语音识别
使用场景会后转写、离线纪要会中字幕、实时提醒
音频输入完整文件URL或base64流式音频帧
调用方式创建任务+轮询WebSocket长连接
结果粒度完整文本、句级时间戳、说话人增量文本、句级时间戳
适合开发门槛较低,封装清晰较高,需要处理连接生命周期

2.3 下游结果处理:不能把转写文本直接当纪要

ASR接口拿到的是“人说了什么”的原始转写文本,不是“会议决定了什么”。两者之间差着一个完整的后处理步骤。会议室里的真实语音充满冗余,比如“呃”“然后”“那个”,还有自我修正、话题跳转。直接把原始转写贴出去,阅读体验非常糟糕。

我的做法是分三层处理转写结果:

  1. 清洗层:去掉语气词、重复片段,按句末标点重新分段
  2. 结构层:利用说话人标记把文本按发言者归组,方便看出谁主导了什么话题
  3. 提取层:设定关键词规则,比如“需要跟进”“下周五之前”“由XX负责”,把行动项和负责人抽出来

这三层用一小段Python脚本就能有一个不错的雏形,后面会给出代码示例。如果团队对纪要质量要求更高,还可以把清洗后的文本交给大模型做摘要和结构化,效果会比纯规则好很多,不过那已经是另一个话题了。

2.4 成本和并发限制要先算清楚

接入云API之前,先估一下自己的使用量级。我自己的模型很简单:一个月10场会议,每场1小时,合计600分钟语音转写量。按云端语音识别常见的按分钟计费方式,这个量级产生的费用非常可控。刚开始测试阶段尽量利用免费额度和低价档位试运行,确定稳定了再批量处理历史录音。

并发和任务数限制也是容易被忽略的点。录音文件识别通常有单账户并发任务上限,如果一次性灌入几十个录音文件,需要做一个简单的提交队列,控制每次最多提交两三个任务,否则会撞到频控报错。这块在第四部分会给出实际建议。

3. 动手接入:用Python调通录音文件识别接口

3.1 安装SDK、开通服务和获取密钥

腾讯云的语音识别接口走的是云API 3.0体系,Python SDK可以直接通过pip安装。我这里安装的是包含语音识别组件的包:

pip install tencentcloud-sdk-python-asr

如果你习惯装全量包,用tencentcloud-sdk-python也可以,只是体积会大不少。建议按产品线装精简包,部署依赖的时候能省很多事。

开通服务这块流程很固定:登录控制台找到语音识别产品,完成开通,然后创建一个用于调用API的子账号密钥。我强烈建议不要直接用主账号的密钥,而是创建一个只授权了语音识别相关服务权限的子账号,这样就算密钥不小心泄露,风险也被限制在一个产品范围内。

控制台里能看到两种密钥形式:SecretId和SecretKey。这两个信息需要保存在服务端环境变量或配置文件中,不要写进代码仓库。我自己的习惯是放在环境变量里,部署的时候再注入。

3.2 录音文件识别的完整调用代码

录音文件识别是一次典型的异步任务。核心流程分三步:创建任务、轮询状态、拉取结果。代码如下:

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 SECRET_ID = "你的SecretId" SECRET_KEY = "你的SecretKey" AUDIO_URL = "https://your-public-host/meeting-20250106.wav" # 1. 初始化客户端 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) # 2. 创建识别任务 req = models.CreateRecTaskRequest() req.EngineModelType = "16k_zh_large" req.ChannelNum = 1 req.ResTextFormat = 1 req.SourceType = 0 # 0=音频URL,1=base64 req.Url = AUDIO_URL resp = client.CreateRecTask(req) task_id = resp.Data.TaskId print("TaskId:", task_id) # 3. 轮询任务状态 while True: treq = models.DescribeTaskStatusRequest() treq.TaskId = task_id tresp = client.DescribeTaskStatus(treq) status = tresp.Data.StatusStr if status == "success": print("识别完成") print(tresp.Data.Result) break elif status == "failed": raise RuntimeError("识别任务失败") time.sleep(3)

几个参数值得展开说明:

  • EngineModelType:引擎模型。我用的16k_zh_large对应16k采样率的中文普通话大模型,会议场景选这个通用性最好。如果音频本身是8k采样率,需要选用对应的8k引擎,否则识别率会受影响
  • ChannelNum:声道数。前面已经预处理成单声道,这里填1。如果填了2,引擎会按双声道分别识别,反而可能导致输出混乱
  • ResTextFormat:结果文本格式。填1会输出带标点的文本,会议纪要场景必须开这个功能
  • SourceType:音频来源。0表示通过公网URL提交,1表示base64编码提交。URL方式对大文件更友好

DescribeTaskStatus轮询返回的Result字段是一个JSON字符串,里面包含了完整的转写结果,通常还有句级时间戳、说话人等信息的具体结构,取决于创建任务时是否开启对应参数。脚本里直接把结果打印出来了,实际使用中可以把它作为字符串传给下游解析。

轮询间隔我这里用的是3秒,实际使用时可以根据任务时长动态调整,一开始可以短一点,1分钟后再拉长间隔,减少无效请求。

3.3 大文件音频怎么提交换行吗

录音文件识别对base64直传的大小有限制,超过限制需要用URL方式,或者把音频先放到对象存储COS,生成一个带有临时签名的访问地址再提交。这也是SourceType=0存在的意义。

如果不想开通对象存储,也可以找一个临时文件托管服务,但要注意两点:一是URL必须公网可访问,二是服务稳定性要足够,否则轮询到一半文件被删了,任务就直接失败。我后来还是选择了对象存储COS,原因很简单:上传完成后生成一个带有效期的签名URL,识别完成后再收回权限,全流程安全可控。

提交代码大概是这个思路:

# 使用对象存储SDK上传文件,获取临时URL from tencentcloud.cos import CosConfig, CosS3Client # 上传后调用generate_presigned_url生成预签名URL # 再把URL传给CreateRecTaskRequest的Url字段

3.4 从转写文本到会议纪要的小脚本

识别结果到手之后,我通常会做一轮后处理。先写一个简单的会议纪要生成脚本,它做的事情很直接:按行读取转写文本,过滤掉语气词和无意义短句,再用规则提取行动项。

import re import json FILTER_WORDS = ["呃", "嗯", "然后", "那个", "这个", "就是"] def clean_line(line: str) -> str: for w in FILTER_WORDS: line = line.replace(w, "") return line.strip() def detect_action(line: str) -> bool: action_pattern = re.compile(r"(需要|负责|跟进|待办|下[周月天]|今天|明天|后天|尽快)") return bool(action_pattern.search(line)) def build_notes(result_text: str) -> dict: raw_lines = result_text.strip().split("\n") clean_lines = [clean_line(l) for l in raw_lines if len(clean_line(l)) > 5] actions = [l for l in clean_lines if detect_action(l)] return { "clean_text": "\n".join(clean_lines), "action_items": actions, } # 假设识别结果存到了变量result_json result_data = json.loads(result_json) transcript = result_data.get("text", "") notes = build_notes(transcript) print(notes["action_items"])

这个脚本非常基础,但已经能应付真实场景中的大多数简单提取需求。如果想更精确地识别“负责人”,还可以维护一个人名名单,通过正则匹配“由某某负责”“让某某跟进”这类句式。更进一步,把清洗后的文本发给大模型,让它输出JSON格式的纪要,这是效果最好的方式。我在内网工具里两种方式都保留了,规则版速度快、稳定,LLM版理解更准确,适合长会议。

4. 识别率优化:让AI听懂你的会议黑话

4.1 用16k引擎、开对参数,剩下的交给热词表

很多刚接入语音识别的开发者,识别率不理想第一反应是换引擎,其实优先应该检查输入音频。音频本身16k采样率、单声道、信噪比足够的话,识别率通常已在一个不错的水准。接下来最值得投入的是热词表,也就是定制词库。会议场景里一定会有产品名、人名、地名、项目代号,这些词通用模型不一定认识。

腾讯云的语音识别提供了热词表能力,可以在控制台或通过API维护一个词表,把公司产品、同事昵称、项目代号、业务黑话加进去。比如我的词表里会有“排期”“链路”“灰度发布”“北极星指标”这类词。加了热词之后,转写文本里这些词的准确率明显提升。

4.2 说话人分离:不是每个场景都能开

会议纪要最希望输出的是“谁说了什么”,这需要语音识别接口开启说话人分离能力。在录音文件识别里,这通常对应一个参数,开启后会在结果中按说话人编号分组,你能拿到每一句对应的SpeakerId。

但这个能力有两个限制要提前知道。一是对话轮次重叠严重时说话人分离准确率会下降,比如几个人同时抢话,引擎容易把人声轨迹分错。二是开启说话人分离对音频质量的要求更高,如果录音是远场拾音或者环境噪声大,效果反而不如普通转写。

我实际验证下来,麦克风质量好的会议室录音开启说话人分离体验很好,能清楚看到“说话人1: 下周三之前要完成上线”“说话人2: 我这边人手不够”。如果是手机录的现场音频,建议先用普通转写把文本跑通,不要强行追求说话人标签。

4.3 通过音频预处理提高识别率的三个细节

预处理不只是格式转换,下面几个细节对识别率有实质性帮助:

  • 音量归一化:会议录音里经常有人说话声音忽大忽小,音频开头和中间响度不均匀。用FFmpeg的loudnorm滤镜做一次响度归一化,能让引擎在统一音量下工作
  • 裁剪静音段:会议中途长时间安静,这个段落不会产生有效文本,但会拉长任务处理时间空闲占用。可以先做静音检测,把超过2秒的静音段压缩
  • 去除回声和背景音乐:如果录音里有持续的背景音乐或风扇噪声,可以用降噪滤镜处理,但注意不要过度降噪导致语音本身受损

降噪和音量处理的FFmpeg命令示例:

ffmpeg -i input.wav -af loudnorm=I=-16:LRA=11:TP=-1.5,highpass=f=200,lowpass=f=8000 -ar 16000 -ac 1 output_clean.wav

这条命令做了三件事:响度归一化、高通滤波滤掉低频噪声、低通滤波滤掉高频干扰,同时保证语音主要能量集中在200Hz到8kHz这个范围内。

5. 高频报错与避坑实录:照着查能省半天

5.1 录音文件识别常见问题速查表

现象可能原因处理方法
创建任务报错,提示认证失败SecretId或SecretKey不正确,或子账号没有语音识别权限检查密钥,给子账号授权相关产品策略
任务一直处于等待状态提交的音频URL不能公网访问,或URL本身失效换成可公网访问的URL,检查CDN或跨域限制
识别结果空白或只有少量文本音频是纯静音或音量极低,识别引擎没捡到有效语音帧先用播放器确认音频有声,再做音量归一化
任务处理时间异常长音频时长太长,或提交任务过多触发排队检查单次音频时长限制,控制并发提交数量
报错提示采样率不支持音频实际采样率与EngineModelType不匹配统一用16k,代码里加校验逻辑
转写文本没有标点ResTextFormat没设置为带标点格式在创建任务时检查该参数

5.2 我踩过的三个坑

第一个坑是采样率不统一导致的识别率暴跌。我有一批老录音是24kHz,当时直接提交给16k大模型,结果很多低频词汇直接被吞掉。后来把预处理流程固定为强制重采样到16k,问题才消失。现在我的工具链里任何音频进来,第一步永远是统一格式,不做完这件事坚决不调用API。

第二个坑是任务并发限制。有次我要转写一个季度累积的二十多段会议录音,脚本里没有加并发控制,一次性把二十多个任务全部提交,结果中途大量任务失败。排查之后才意识到需要给提交逻辑加上一个简单的队列流量控制,把并发控制在两三个以内,还要在失败时做指数退避重试。

第三个坑是回调地址的问题。录音文件识别支持设置回调URL,我一开始图省事,在自己的内网测试环境里放了一个回调地址,结果根本收不到通知。排查后发现回调地址必须公网可达,而且服务端需要返回HTTP 200,否则会判定回调失败。后来我改成轮询模式,开发阶段最稳定,也最容易定位问题。

5.3 轮询比回调省心,但要注意写对状态判断

在开发阶段我更推荐轮询,逻辑直观、不依赖外网回调。轮询时状态判断只认两个最终状态,success和failed。中间状态还有类似waiting、processing之类,但没必要为每个中间状态写分支,只有最终状态才需要处理。为了减少无效请求,轮询间隔可以做成动态的:前三次间隔2秒,之后每次间隔5秒,超过两分钟还没完成就改成10秒。识别一个一小时的录音,完整等待时间通常在几分钟量级。

5.4 把脚本固化成一个命令行工具

调用逻辑验证通过后,我把它封装成一个简单的命令行工具,方便每次会议结束后直接调用。命令大概是:

python meeting_assistant.py --input ./meeting_0115.wav --output ./notes_0115.md

脚本内部做五件事:预处理音频、上传对象存储、创建识别任务、轮询结果、生成纪要Markdown。命令行入口的好处是,团队的同事不需要理解API细节,只要把录音文件路径传进去就能得到纪要。这其实就是“会议小帮手”项目最核心的交付物,不复杂但很实用。

使用了一段时间之后,我最大的体会是:语音识别本身已经足够成熟,真正决定这个工具好不好用的,永远是前面音频是否合格、后面结果是否做了有效的结构化解读。如果你也想做一个类似的会议小帮手,我建议动手时先跑通一条最小链路:拿一段清晰的录音,走通提交到出文本,再花点时间调后处理规则。先把这段路走顺了,再去纠结说话人分离、热词优化这些进阶能力,会遇到更多具体问题,到那时手里已经有了能复现真实场景的基础设施,做任何调优都会有的放矢。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进