ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

腾讯云AI语音识别实战:从录音到会议纪要的完整方案

腾讯云AI语音识别实战:从录音到会议纪要的完整方案

1. 会议记录这件事,到底卡在哪儿了

开完一场两小时的跨部门对齐会,回到工位上盯着空白的会议纪要文档,脑子里只剩下零星的几个关键词——这种体验我相信大多数职场人都经历过。更别提那些远程会议,录音文件躺在硬盘里,想回头找某个决策点,得从头到尾拖进度条,效率低到让人抓狂。我身边不少朋友试过各种办法:有人专门安排一个同事做记录,结果那个人全程低头打字,根本没法参与讨论;有人用手机录音,事后靠耳朵一句句听,两小时的会要花三四个小时整理;还有人干脆放弃记录,全凭记忆推进工作,最后扯皮的时候谁也说不清当时到底定了什么。

这个项目的出发点特别朴素:能不能让机器帮我们把会议内容自动转成文字,顺便把关键信息也提取出来?答案是可以的,而且门槛比很多人想象的低得多。腾讯云AI语音识别提供了一套完整的语音转文字能力,我们只需要围绕它搭一个轻量的“会议小帮手”,就能实现录音上传、自动转写、关键词提取、摘要生成这一整条链路。说白了,就是把原本需要人工花几个小时做的事情,压缩到几分钟内自动完成。

这篇文章适合谁看?如果你是开发者,想找一个完整的语音识别落地案例练手,这篇会给你从架构设计到代码实现的全部细节;如果你是产品经理或团队负责人,想了解这套方案能做到什么程度、成本大概多少,也能从中找到答案;哪怕你只是对AI语音识别好奇,想自己动手做一个能用的工具,跟着走一遍也能跑通。我会把踩过的坑、参数怎么调、代码怎么写、效果怎么优化,全部摊开来讲,不藏私。

2. 整体方案设计:为什么这样搭最省事

2.1 核心需求拆解与功能边界

先把需求理清楚。一个“会议小帮手”到底要做什么?我把它拆成了四个核心模块:

  • 音频输入:支持上传本地录音文件,也支持实时录制。实际场景里,大多数会议是通过会议软件录制的,导出为mp3或wav格式,所以文件上传是刚需。
  • 语音转文字:这是核心中的核心,把音频里的说话内容转成带标点的文本,最好还能区分不同说话人。
  • 文本后处理:转写出来的原始文本是一大坨,需要做断句、分段、关键词提取、摘要生成,才能变成人能快速阅读的会议纪要。
  • 结果输出:生成结构化的会议纪要文档,支持导出和分享。

功能边界也要明确:我们不做实时字幕(那个对延迟要求太高,架构完全不同),不做多语言翻译(虽然腾讯云支持,但会增加复杂度),不做声纹识别(区分具体是谁说的,那个需要预先注册声纹)。先把核心链路跑通,再考虑扩展。

2.2 技术选型:为什么是腾讯云AI语音识别

市面上语音识别的方案不少,开源的像Whisper、Kaldi,云服务像腾讯云、阿里云、讯飞。我选腾讯云AI语音识别,主要基于几个实际考量:

第一,中文识别准确率。我实测过几段带口音的会议录音,腾讯云在中文场景下的表现明显更稳。特别是涉及到专业术语和缩略词的时候,它的一站式识别能力比通用模型强不少。Whisper虽然开源免费,但中文标点符号的处理一直是个痛点,经常出现整段没有标点的情况,后处理成本很高。

第二,说话人分离能力。腾讯云的录音文件识别支持开启说话人分离,返回的结果里会标注每句话是谁说的(speaker 0、speaker 1……)。这个功能对会议场景太重要了,否则转出来的文本就是一团乱麻,根本分不清谁提出了什么观点。

第三,接入成本低。腾讯云提供完整的SDK和API文档,Python SDK几行代码就能调通。对于个人开发者和小团队来说,不需要自己训练模型、不需要买GPU服务器,按量付费,用多少算多少。

第四,生态完整。除了语音识别,腾讯云还有自然语言处理、文本摘要等能力,后续要做关键词提取和摘要生成,可以直接在同一个体系里完成,不用来回切换平台。

当然,选型不是绝对的。如果你对数据隐私要求极高,必须本地部署,那开源方案更合适;如果你预算充足且对实时性要求高,可以考虑实时语音识别接口。但对于大多数会议记录场景,录音文件识别+后处理的方案,性价比是最高的。

2.3 系统架构与数据流转

整个系统的架构不复杂,我画一个文字版的流程:

用户上传音频 → 对象存储(COS)→ 调用语音识别API → 获取转写结果(JSON) → 文本清洗与分段 → 关键词提取 → 摘要生成 → 生成会议纪要 → 前端展示/导出

这里有几个关键设计决策:

为什么用对象存储中转?腾讯云的录音文件识别API要求音频文件可以通过URL访问。直接把本地文件传上去不行,得先传到对象存储(COS)生成一个临时链接,再把链接传给识别接口。这个设计虽然多了一步,但好处是识别任务可以异步处理,不用一直等着。

为什么用异步识别而不是同步?同步接口有时长限制(一般60秒以内),会议录音动辄半小时起步,必须用异步接口。异步接口的流程是:提交任务→轮询结果→获取转写文本。虽然多了一次轮询,但稳定性和成功率都高得多。

后处理为什么单独做?语音识别返回的结果是带时间戳的句子列表,直接拼接可读性很差。需要做几件事:按时间间隔和语义断句、合并同一说话人的连续发言、去掉语气词和重复内容。这些逻辑放在后端单独处理,方便调试和优化。

3. 核心细节解析:从音频到文字的每一步

3.1 音频格式与参数选择

音频质量直接决定识别准确率。我踩过的坑里,至少有一半是因为音频本身有问题。先说格式:腾讯云支持wav、mp3、m4a、aac等常见格式,但推荐用wav或无损格式,因为mp3是有损压缩,高频部分被砍掉后会影响识别。如果原始录音是mp3,尽量找高码率的版本,128kbps以下的建议先转码。

采样率方面,16kHz是黄金标准。人声的主要频率范围在300Hz到3400Hz之间,16kHz采样率完全覆盖,而且文件体积适中。如果录音是8kHz(电话录音常见),识别率会明显下降,特别是区分“四”和“十”这种发音相近的词。如果是48kHz的高保真录音,建议降采样到16kHz,既减小文件体积,又不影响识别效果。

声道数也有讲究。单声道比立体声更适合语音识别。立体声虽然听起来更有空间感,但两个声道的内容如果相位不一致,反而会干扰识别引擎。如果原始录音是立体声,建议在预处理阶段合并为单声道。

还有一个容易被忽略的点:音量归一化。有些会议录音里,离麦克风远的人声音很小,近的人声音很大。识别引擎对音量过小的片段识别率很差。我一般会用音频处理库做一次音量归一化,把所有片段的音量调整到大致相同的水平。这个操作在Python里用pydub或者librosa都能做,几行代码的事。

3.2 调用语音识别接口的关键参数

腾讯云录音文件识别的接口参数不少,但真正影响效果的就这么几个:

参数名推荐值作用不设置的后果
EngineModelType16k_zh指定中文通用模型默认可能是英文模型,中文识别乱码
ChannelNum1声道数立体声不合并会报错或识别混乱
ResTextFormat2返回带时间戳的详细结果只返回纯文本,无法做说话人分离
SpeakerDiarizationtrue开启说话人分离分不清谁说的,纪要没法整理
SpeakerNumber0自动判断说话人数手动指定容易猜错,自动更稳
FilterDirty1过滤脏词会议里偶尔的脏话会被转出来
FilterModal1过滤语气词“嗯”“啊”“那个”全保留,文本很啰嗦
FilterPunc0保留标点设成1会把标点全去掉,可读性极差
ConvertNumMode1数字转阿拉伯数字“一百二十三”不如“123”直观

这些参数里,SpeakerDiarization和ResTextFormat是最关键的两个。说话人分离不开,会议纪要就是一团乱麻;ResTextFormat不设成2,拿不到时间戳,后续做分段和摘要都无从下手。

还有一个隐藏参数:HotwordList。这是热词表,可以传入会议中可能出现的专业术语、人名、产品名。比如你们公司内部有个项目叫“天枢”,识别引擎可能听成“天数”“天书”,把“天枢”加到热词表里,识别准确率立刻上去。热词表最多支持1000个词,每个词可以设置权重,权重越高越容易被识别出来。我一般会把公司名、产品名、常见人名、行业术语都加进去,效果提升非常明显。

3.3 说话人分离的原理与局限

说话人分离(Speaker Diarization)听起来很玄乎,其实原理不复杂:先提取音频中的声纹特征,然后用聚类算法把相似的声纹归为一类,每一类就是一个说话人。腾讯云用的是深度学习模型做声纹提取,比传统的MFCC+GMM方案准确率高不少。

但要注意几个局限:

第一,说话人数超过5个之后准确率下降。这是聚类算法的通病,类别越多越容易混淆。如果会议超过5个人,建议提前指定SpeakerNumber,或者接受一定的误判率。

第二,短发言容易被归错。如果一个人只说了“对”“好的”这种一两个字的回应,声纹特征太少,很容易被归到别人身上。这个目前没有太好的解决办法,只能在后处理阶段人工修正。

第三,重叠说话处理不好。两个人同时说话的时候,识别引擎只能选一个主要声源,另一个人的话会丢失。这是所有语音识别系统的通病,不是腾讯云独有的问题。如果会议经常出现抢话的情况,建议在会议规范上要求轮流发言。

3.4 文本后处理的核心逻辑

语音识别返回的原始结果大概长这样:

{ "Result": { "Sentences": [ {"Text": "那个我们今天讨论一下这个季度的目标", "StartTime": 0, "EndTime": 2500, "SpeakerId": 0}, {"Text": "好的我觉得应该先把上个月的复盘做一下", "StartTime": 2600, "EndTime": 5200, "SpeakerId": 1}, ... ] } }

直接把这些句子拼起来,可读性很差。后处理要做几件事:

断句与分段:按时间间隔断句,如果两句话之间间隔超过1.5秒,认为是两个独立的段落。同时按说话人分段,同一个人连续说的几句话合并成一个段落。

语气词过滤:虽然接口有FilterModal参数,但过滤得不够干净。我会在后处理阶段再用正则表达式去掉“嗯”“啊”“那个”“就是说”这类词。注意不要过度过滤,有些语气词去掉后句子会不通顺。

重复内容合并:会议里经常有人说“对对对”“好好好”,这些重复内容要合并成“对”“好”。

标点修正:识别引擎加的标点不一定准确,特别是问号和句号容易混淆。我会用简单的规则做二次修正,比如句尾有“吗”“呢”“吧”的改成问号。

数字与单位规范化:把“百分之三十”转成“30%”,“三点五”转成“3.5”,这些细节能大幅提升可读性。

4. 实操过程:从零搭建会议小帮手

4.1 环境准备与依赖安装

先列一下需要的东西:

  • Python 3.8以上
  • 腾讯云账号,开通语音识别和对象存储服务
  • 腾讯云Python SDK:pip install tencentcloud-sdk-python
  • 音频处理库:pip install pydub librosa
  • Web框架(可选):pip install flask

获取密钥的步骤我就不赘述了,在腾讯云控制台的访问管理里创建API密钥,拿到SecretId和SecretKey。建议把这些敏感信息放在环境变量里,不要硬编码在代码中。

对象存储需要创建一个存储桶(Bucket),记住Bucket名称和所属地域(Region)。上传文件后生成的临时链接有效期默认是1小时,足够识别任务完成了。

4.2 音频上传与预处理代码

先写一个音频预处理的函数,把各种格式统一成16kHz单声道的wav:

from pydub import AudioSegment import os def preprocess_audio(input_path, output_path): """ 将音频统一转换为16kHz单声道wav格式 """ audio = AudioSegment.from_file(input_path) # 转为单声道 if audio.channels > 1: audio = audio.set_channels(1) # 设置采样率为16kHz audio = audio.set_frame_rate(16000) # 音量归一化,目标音量-20dBFS target_dBFS = -20.0 change_in_dBFS = target_dBFS - audio.dBFS audio = audio.apply_gain(change_in_dBFS) # 导出为wav audio.export(output_path, format="wav") return output_path

这段代码里,音量归一化那一步很关键。我试过不归一化直接识别,远场说话人的内容经常丢失。归一化之后,识别完整度提升了大概20%。

4.3 调用识别接口的完整流程

接下来是核心的识别调用。腾讯云的异步识别分三步:提交任务、查询状态、获取结果。

from tencentcloud.common import credential from tencentcloud.asr.v20190614 import asr_client, models import json import time def create_asr_task(audio_url): """ 提交录音文件识别任务 """ cred = credential.Credential( os.environ.get("TENCENT_SECRET_ID"), os.environ.get("TENCENT_SECRET_KEY") ) client = asr_client.AsrClient(cred, "ap-guangzhou") req = models.CreateRecTaskRequest() req.EngineModelType = "16k_zh" req.ChannelNum = 1 req.ResTextFormat = 2 req.SourceType = 0 req.Url = audio_url req.SpeakerDiarization = True req.SpeakerNumber = 0 req.FilterDirty = 1 req.FilterModal = 1 req.FilterPunc = 0 req.ConvertNumMode = 1 # 热词表 req.HotwordList = ["天枢", "星链计划", "OKR", "复盘"] resp = client.CreateRecTask(req) return resp.Data.TaskId def wait_for_result(task_id, max_wait=600): """ 轮询识别结果,最多等待10分钟 """ cred = credential.Credential( os.environ.get("TENCENT_SECRET_ID"), os.environ.get("TENCENT_SECRET_KEY") ) client = asr_client.AsrClient(cred, "ap-guangzhou") start_time = time.time() while time.time() - start_time < max_wait: req = models.DescribeTaskStatusRequest() req.TaskId = task_id resp = client.DescribeTaskStatus(req) status = resp.Data.Status if status == 2: # 识别完成 return json.loads(resp.Data.Result) elif status == 3: # 识别失败 raise Exception(f"识别失败: {resp.Data.ErrorMsg}") time.sleep(5) # 每5秒轮询一次 raise Exception("识别超时")

这里有几个实操细节值得说:

轮询间隔设5秒比较合适。设太短浪费请求次数,设太长结果出来得慢。实测下来,1小时的录音大概3-5分钟能识别完,5秒轮询完全够用。

热词表要提前准备。我一般会在项目启动时维护一个热词文件,每次调用时读取。热词不要太多,100个以内效果最好,太多会稀释权重。

错误处理要做好。识别失败的原因很多:音频格式不对、文件太大、网络超时。每种错误要有对应的提示,方便排查。

4.4 文本后处理与纪要生成

拿到识别结果后,开始后处理:

import re def merge_sentences(sentences, gap_threshold=1500): """ 按时间间隔和说话人合并句子 """ merged = [] current = None for sent in sentences: if current is None: current = { "speaker": sent["SpeakerId"], "text": sent["Text"], "start": sent["StartTime"], "end": sent["EndTime"] } else: gap = sent["StartTime"] - current["end"] same_speaker = sent["SpeakerId"] == current["speaker"] if same_speaker and gap < gap_threshold: current["text"] += sent["Text"] current["end"] = sent["EndTime"] else: merged.append(current) current = { "speaker": sent["SpeakerId"], "text": sent["Text"], "start": sent["StartTime"], "end": sent["EndTime"] } if current: merged.append(current) return merged def clean_text(text): """ 清洗文本:去语气词、合并重复、修正标点 """ # 去掉常见语气词 fillers = ["嗯", "啊", "那个", "就是说", "然后呢", "这个呢"] for filler in fillers: text = text.replace(filler, "") # 合并重复字符 text = re.sub(r"(.)\1{2,}", r"\1", text) # 修正标点 text = re.sub(r"([吗呢吧])。", r"\1?", text) # 去掉多余空格 text = re.sub(r"\s+", "", text) return text.strip()

后处理完之后,生成结构化的会议纪要:

def generate_minutes(merged_sentences): """ 生成会议纪要 """ minutes = [] speaker_map = {} # 说话人ID映射为"发言人A/B/C" for sent in merged_sentences: speaker_id = sent["speaker"] if speaker_id not in speaker_map: speaker_map[speaker_id] = f"发言人{chr(65 + len(speaker_map))}" speaker_name = speaker_map[speaker_id] text = clean_text(sent["text"]) if text: # 过滤空内容 minutes.append(f"**{speaker_name}**:{text}") return "\n\n".join(minutes)

4.5 关键词提取与摘要生成

会议纪要光有全文还不够,最好能自动提取关键词和摘要。腾讯云的自然语言处理接口可以做这件事,但为了减少依赖,我用了一个轻量方案:基于TF-IDF的关键词提取。

from sklearn.feature_extraction.text import TfidfVectorizer import jieba def extract_keywords(text, top_k=10): """ 基于TF-IDF提取关键词 """ # 分词 words = jieba.lcut(text) words = [w for w in words if len(w) > 1] # 过滤单字 # 用TF-IDF计算权重 vectorizer = TfidfVectorizer() tfidf_matrix = vectorizer.fit_transform([" ".join(words)]) # 获取特征词和对应的TF-IDF值 feature_names = vectorizer.get_feature_names_out() tfidf_scores = tfidf_matrix.toarray()[0] # 排序取Top K word_scores = list(zip(feature_names, tfidf_scores)) word_scores.sort(key=lambda x: x[1], reverse=True) return [word for word, score in word_scores[:top_k]]

摘要生成用TextRank算法,原理是把句子作为节点,句子之间的相似度作为边,然后迭代计算每个句子的重要性。这个算法在jieba里已经有实现:

import jieba.analyse def generate_summary(text, top_k=5): """ 基于TextRank生成摘要 """ # 分句 sentences = re.split(r"[。!?]", text) sentences = [s.strip() for s in sentences if len(s.strip()) > 10] if len(sentences) <= top_k: return "。".join(sentences) + "。" # 用TextRank提取关键句 summary_sentences = jieba.analyse.textrank( "。".join(sentences), topK=top_k, withWeight=False ) return "。".join(summary_sentences) + "。"

这两个方案虽然不是最先进的,但胜在轻量、无需额外API调用、效果也够用。如果对摘要质量要求更高,可以接入腾讯云的文本摘要接口,那个是基于大模型的,效果更好但成本也更高。

5. 常见问题与排查技巧实录

5.1 识别准确率低的排查思路

识别准确率低是最常见的问题,原因可能有很多。我整理了一个排查清单:

现象可能原因排查方法解决方案
整段识别为空音频格式不支持用播放器确认文件能正常播放转码为16kHz单声道wav
部分内容丢失音量过小用音频软件查看波形做音量归一化
专业术语识别错未配置热词检查热词表是否包含该词添加热词并设置高权重
说话人混淆说话人数过多确认实际说话人数指定SpeakerNumber或减少参会人数
标点混乱FilterPunc设置错误检查参数是否为0设为0保留标点
数字识别成中文ConvertNumMode未开启检查参数设为1转阿拉伯数字

我遇到最坑的一次是音频文件本身有问题——录音设备设置了自动增益,导致音量忽大忽小,识别引擎在音量骤降的片段直接跳过了。后来在预处理阶段加了动态范围压缩,问题才解决。

5.2 接口调用超时与限流处理

腾讯云语音识别接口有默认的QPS限制,具体数值取决于你的账号等级。如果并发提交太多任务,会返回RequestLimitExceeded错误。解决办法有两个:

第一,加队列。把识别任务放进消息队列,控制并发数。我用的是简单的Redis队列,每次只提交3个任务,完成一个再提交下一个。

第二,加退避重试。遇到限流错误时,等待一段时间再重试,等待时间指数增长:

import time def call_with_retry(func, max_retries=3): for i in range(max_retries): try: return func() except Exception as e: if "RequestLimitExceeded" in str(e) and i < max_retries - 1: wait_time = 2 ** i # 1s, 2s, 4s time.sleep(wait_time) else: raise

5.3 长音频处理的注意事项

超过1小时的音频,识别时间会比较长,而且失败率也会上升。我的经验是:

超过2小时的音频,建议先切分。用音频处理库按静音片段切分成30分钟左右的段落,分别识别后再合并。这样即使某一段失败,也不用全部重来。

切分点要选在静音处。如果在说话中间切开,两段的首尾都会丢失内容。用pydub的silence检测功能可以找到静音点:

from pydub import AudioSegment from pydub.silence import detect_silence def split_audio(audio_path, min_silence_len=1000, silence_thresh=-40): audio = AudioSegment.from_file(audio_path) silences = detect_silence(audio, min_silence_len=min_silence_len, silence_thresh=silence_thresh) # 在静音中点切分...

5.4 成本控制与优化建议

腾讯云语音识别按音频时长计费,具体价格在官网可以查到。如果会议量大,成本还是要算一算的。几个省钱技巧:

第一,能复用的不重复识别。同一段音频不要重复提交,结果存数据库,下次直接查。

第二,短会议用同步接口。如果会议在1分钟以内,用同步接口更便宜(同步接口按次计费,异步按分钟计费)。

第三,非关键会议降低采样率。8kHz的识别费用比16kHz低,如果只是内部同步会,对准确率要求不高,可以用8kHz。

第四,定期清理对象存储。识别完成后,音频文件就没用了,及时删除,避免存储费用累积。

5.5 几个让我印象深刻的坑

坑一:中文文件名导致上传失败。对象存储的URL不支持中文,上传前要把文件名转成英文或拼音。我一开始没注意,调试了半天才发现是文件名的问题。

坑二:时区问题导致时间戳错乱。识别结果的时间戳是相对于音频开始的毫秒数,但如果你在展示时用了本地时区转换,会全部错位。记住:时间戳就是毫秒数,不要做任何时区转换。

坑三:并发写入导致数据覆盖。多个识别任务同时完成,如果都往同一个文件里写结果,会互相覆盖。解决办法是每个任务写独立的文件,最后再合并。

坑四:热词权重设置过高导致误识别。热词权重最高可以设到10,但设太高会让引擎强行把不相关的词识别成热词。我一般设到5-8之间,平衡准确率和误识别率。

6. 后续可以怎么扩展

这套方案跑通之后,能扩展的方向很多。比如接入实时语音识别接口,做会议实时字幕;比如接入大模型做智能问答,会后直接问“这次会议关于预算的结论是什么”;比如对接企业微信或飞书,自动把会议纪要推送到群里。

我个人最想加的功能是自动待办提取。会议里经常有人说“这个事我来跟进”“下周三之前给方案”,这些承诺如果能自动识别出来并生成待办列表,那会议小帮手就真的变成生产力工具了。技术上不难,用正则匹配“我来”“我负责”“之前给”这类模式,再结合时间实体识别,就能提取出待办事项和截止时间。

还有一个方向是多会议关联分析。同一个项目每周开一次会,把历次会议纪要串起来,就能看到决策的演变过程。这个需要做会议主题聚类和时序分析,复杂度高一些,但价值也更大。

最后分享一个小技巧:如果你觉得识别出来的文本还是太啰嗦,可以在后处理阶段加一个“压缩”步骤,用大模型把每段话压缩成一句话。我试过用提示词“把以下会议发言压缩成不超过20字的要点”,效果出奇地好,纪要篇幅直接减半,可读性反而提升了。

RELATED READING

延伸阅读

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