
做了很多次项目之后我发现会议纪要这件事看着不起眼实际上特别耗人。无论参会时记得多仔细最后整理录音还是得一秒一秒听碰到开放式讨论更是噩梦。后来我用腾讯云AI语音识别搭了一个会议小帮手把录音丢进去自动转出带时间戳、带说话人标记的文字稿再稍微清洗一下就能直接变成会议纪要。原本一小时起步的整理工作现在十分钟内搞定。这篇就聊聊我是怎么选的方案、怎么写的代码、踩了哪些坑如果你也想给团队做类似工具可以直接参考。整个项目不复杂但接口参数、结果解析、音频预处理这些环节里的细节不少。适合有Python基础、想用云服务快速落地工具的开发者也适合项目助理之类的角色理解原理。1. 先把需求说清楚会议小帮手到底要干什么1.1 会议记录的真实痛点开会这件事真正的成本往往在会后。手动整理录音时你需要反复回放、辨别谁在说话、把口语内容转成书面语还要挑出待办事项。一场四十分钟的会议整理纪要花一个多小时很正常。很多团队干脆不整理结果就是会上聊得很热闹会后大家各忙各的决议项没人跟进。我一开始也没有想做一个多复杂的系统只是想要一个“能把录音变成文字”的工具解决最痛的那一步。后来发现只要能把语音转文字的环节自动化后面很多事都能顺带做按说话人分段、提取关键词、找待办事项、生成标准格式的会议纪要这些都是文本处理问题好解决得多。真正难的是从语音到文字的这一步。1.2 最小可用版本应该包含什么给项目定边界是很重要的一件事否则很容易从一个转写工具膨胀成一套“会议AI平台”。我的MVP选了三块功能录音文件自动转写支持常见格式上传后异步识别。说话人区分和分段能分辨出谁说了什么至少能按人分段。纪要导出把识别结果整理成带时间戳的Markdown或纯文本文件直接能发进工作群。实时字幕、会议摘要、待办自动提取、自动推送这些全部放到二期。这样项目第一版的范围非常可控一个人几天就能写完核心流程。如果你也给团队做建议先问清楚他们手头是录音文件比较多还是需要现场转写是单人会较多还是多人讨论多这两个问题直接决定了下面的方案选型问错了后面全要返工。1.3 为什么选云端API而不是自建模型我自己也研究过本地部署ASR模型但很快就放弃了。原因很直接本地模型要环境、要GPU、要语料调优识别效果还不一定比得上云端大厂经过海量数据训练的服务。更关键的是会议室场景有大量中文口语、专有名词和中英混说通用本地小模型经常翻车。腾讯云的AI语音识别服务有几个点比较吸引我接口简单Python SDK封装得很完整同时提供录音文件识别和实时语音识别两条路径可以覆盖会前、会中、会后三个阶段中英文混合和普通话识别在实际测试里表现稳定按量计费不跑任务就不花钱。对个人和中小团队来说把语音识别作为基础设施调用比自己从头做划算太多。提示如果你们有严格的音频数据合规要求需要先确认云端的处理协议是否满足再决定是否使用云端API。这一点不在技术范围内但很重要。2. 方案选型两个关键接口和几个关键参数2.1 录音文件识别和实时语音识别怎么选腾讯云语音识别里会议场景最常打交道的是两个接口录音文件识别把完整音频文件传过去异步返回识别结果。适合会后整理。实时语音识别通过WebSocket流式上传音频一边说话一边出字。适合现场投屏字幕、实时转写。我第一版直接选了录音文件识别。原因很现实大部分会议用的是线上会议软件自带的录制功能会后能导出音频文件即使线下会议也可以用录音笔先录下来会后统一处理。实时识别的接入复杂度和稳定性要求都更高后续有需要再补。如果你做的场景是需要现场展示字幕、或者希望开会时就能看到结构化记录那就直接上实时识别别绕弯。两个接口可以共用一套后续的文本处理逻辑区别主要在音频输入链路。2.2 音频格式和采样率到底怎么设不管是哪种识别方式音频质量决定了识别效果的天花板。先说采样率。腾讯云的引擎模型分8k和16k两档电话录音通常是8k正常会议录音建议选16k信息量更大。判断音频采样率可以用ffprobeffprobe -v quiet -print_format json -show_streams meeting.mp3 | grep sample_rate如果源文件采样率是48k或44.1k直接提交给识别接口一般也能处理但官方建议压缩到16k wav或者mp3可以减少传输和计算成本识别速度也会快一些。再说声道。很多会议录音是双声道一个声道对应一个发言人这对说话人分离特别有帮助。接口里ChannelNum参数可以设置双声道识别同时如果开启说话人分离声道信息能明显提高正确率。我实测过单声道和双声道识别同一场双人会议双声道模式下说话人切分基本不用人工修正单声道模式偶尔会把两个人的话串成一团。2.3 说话人分离效果和限制都要心里有数说话人分离是会议纪要的刚需。腾讯云录音文件识别支持通过SpeakerDiarization等相关参数开启识别结果里会给每一句打上说话人编号。但是这里有个容易踩的坑说话人分离不是所有引擎都支持而且默认情况下不是所有参数组合都打开。提交任务前要确认自己选择的EngineModelType和参数组合是否支持否则任务能成功但返回结果里根本没有SpeakerId字段你还得回头重新提交。另一个限制是说话人分离对重叠说话很敏感。两个人同时说话时算法大概率会归给音量更大的那个。所以如果需要相对干净的说话人分段拾音时最好让每个人使用独立麦克风或者用双声道录制这样后期切成两路单独识别效果比任何算法都稳。3. 代码落地录音文件识别从提交到出稿3.1 环境准备SDK安装与最小权限配置实际动手前先把环境搞定。pip install tencentcloud-sdk-python-asr如果对版本敏感可以固定一个已知稳定的版本避免SDK升级导致接口字段变化。接着是密钥。个人开发图省事可以直接用主账号的SecretId和SecretKey但稍微正规一点的项目建议在访问管理控制台创建一个子账号只授予语音识别相关的读写权限比如QcloudASRFullAccess。这样即使密钥泄露影响范围也可控。密钥千万别写进代码仓库我习惯放到环境变量里export TENCENT_SECRET_ID你的SecretId export TENCENT_SECRET_KEY你的SecretKey音频文件建议先传到对象存储生成临时访问URL再提交识别任务。录音文件往往几十上百MB如果走Base64上传很容易超出请求体限制而且传输很慢。3.2 提交识别任务录音文件识别是异步任务先提交拿到TaskId再轮询状态。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() req.EngineModelType 16k_zh req.ChannelNum 1 req.ResTextFormat 1 req.SourceType 1 req.Url https://your-bucket.cos.ap-guangzhou.myqcloud.com/meeting_20250101.mp3 # 如果开启了说话人分离根据SDK版本设置对应字段 # req.SpeakerDiarization 1 resp client.CreateRecTask(req) task_id resp.Data.TaskId print(task_id:, task_id)这里面几个参数说下EngineModelType模型类型16k_zh是最常用的中文普通话模型。如果会议里有很多英文可以选带英文支持的模型。ChannelNum声道数真实音频是双声道就设2是单声道就设1别瞎填。ResTextFormat返回文本格式1表示返回带标点和时间戳的结构化文本做纪纪要必须设1。SourceType1表示传URL0表示传Base64数据。3.3 轮询结果并解析提交任务之后不能干等要用TaskId去查状态。注意任务处理时长和音频时长有关几分钟的会议录音可能几秒就出结果一个小时的录音可能要等一两分钟。import time import json query_req models.DescribeTaskStatusRequest() query_req.TaskId task_id while True: query_resp client.DescribeTaskStatus(query_req) data query_resp.Data if data.StatusStr success: print(识别完成) result data.Result break elif data.StatusStr failed: print(识别失败, data.ErrorMsg) result None break else: print(任务状态, data.StatusStr) time.sleep(3)拿到result之后它是一个字符串里面可能是JSON编码的句子列表也可能直接是纯文本取决于你提交时填的ResTextFormat以及具体返回版本。我这里按最常见的结构化返回写解析逻辑。ResTextFormat设为1时返回结果里通常包含SentenceList之类的字段每个句子有开始时间、结束时间、说话人编号和文本内容if result: parsed json.loads(result) if isinstance(parsed, str): parsed json.loads(parsed) sentences [] # 具体取数逻辑以实际返回结构为准这里做兼容处理 if isinstance(parsed, list): sentences parsed elif isinstance(parsed, dict): sentences parsed.get(SentenceList) or parsed.get(Sentences) or [] for s in sentences: speaker s.get(SpeakerId, 0) start_ms s.get(StartTime, 0) end_ms s.get(EndTime, 0) text s.get(Text, ) print(f[{start_ms / 1000.0:.1f}s-{end_ms / 1000.0:.1f}s] 说话人{speaker}: {text})不同版本的返回结构会有差异我第一次接的时候就被这个坑过我以为Result直接是JSON列表结果外面还包了一层字符串json.loads之后还是一个字符串又loads了一次才拿到真正的数据。写代码时最好打印一下原始返回先肉眼确认结构再写解析。3.4 清洗成一份能交差的会议纪要拿到句子列表之后离“能直接发群里”的会议纪要还差两步按说话人聚合段落以及清洗口语词。按说话人聚合的思路是同一个说话人的连续句子合并成一个发言块说话人切换时另起一段。我写了这样一个函数def build_blocks(sentences): blocks [] current_speaker None current_start None current_texts [] def flush(): if current_speaker is None: return start_t current_start / 1000.0 if current_start else 0 content .join(current_texts) blocks.append({ speaker: current_speaker, start: start_t, text: content }) for s in sentences: sp s.get(SpeakerId, 0) start s.get(StartTime, 0) text s.get(Text, ).strip() if not text: continue if sp ! current_speaker: flush() current_speaker sp current_start start current_texts [] current_texts.append(text) flush() return blocks清洗口语词要克制。像“嗯”“呃”“那个”“就是说”这种纯语气词可以正则去掉但“然后”这种词有时候是正常逻辑连接去掉反而影响阅读。我一般只清理无意义语气词不做太激进的改写。import re def clean_text(text): text re.sub(r(嗯|呃|那个|就是说), , text) text re.sub(r\s, , text) return text.strip()最后输出Markdown直接可发群def to_markdown(blocks): lines [# 会议纪要, ] for b in blocks: lines.append(f#### 说话人{b[speaker]}{b[start]:.0f}s 开始) lines.append(b[text]) lines.append() return \n.join(lines)这样一份带时间戳、带说话人的纪要就出来了。排版很简单但够用。4. 进阶优化热词表、实时转写和会后检索4.1 热词表能让专业名词识别率明显上升会议里最头疼的往往不是普通口语而是项目代号、人名、产品版本号这些专有名词。比如你说“Q3版本有几个阻塞缺陷”模型很可能把“Q3”识别成“球三”或者“Q三”。腾讯云语音识别控制台提供了热词表功能。你可以建一张表把团队常用的词都填进去还可以设置权重权重越高模型越倾向识别成这个词。我实际建的热词表大概是这样的“项目代号X” 权重10“某某模块” 权重8团队成员昵称 权重5竞品名称 权重3建完之后会得到一个HotwordId在提交识别任务时传进去req.HotwordId 你的热词表ID实测效果非常明显。加了热词表之后原来经常识别错的产品名和版本号基本不再翻车人名也从“某工”变成了正确昵称。建议第一次使用前就把团队高频词维护好这个投入产出比极高。4.2 实时转写场景怎么接如果你需要现场字幕或者希望会议结束立即就能出纪要就得接实时语音识别。它的本质是WebSocket流式上传音频腾讯云边收边识别延迟一般在几百毫秒到一两秒。实时识别需要一个带签名的WebSocket地址签名计算逻辑比录音文件识别复杂一些官方Python SDK里有示例建议直接参考不要自己造签名轮子。流程大致是这样import websocket import json import time # ws_url 需要按照官方签名规则生成这里省略 ws websocket.create_connection(ws_url) # 将音频分片发送这里以读取PCM文件为例 with open(segment.pcm, rb) as f: while chunk : f.read(3200): ws.send_binary(chunk) time.sleep(0.1) # 模拟实时推送节奏 # 发送结束标志 ws.send(json.dumps({type: end})) while True: try: msg ws.recv() resp json.loads(msg) if resp.get(code) ! 0: break text resp.get(result, {}).get(text, ) if text: print(text, flushTrue) if resp.get(final): break except Exception: break ws.close()上面只是流程骨架实际用的时候音频分片的大小、发送间隔、断线重连机制都要根据网络情况调整。如果网络不稳定建议加一层本地缓存把识别结果先落盘避免丢字。4.3 让纪要能搜、能管转写只是第一步会议纪要的价值在于事后能查。我第二版把生成的Markdown解析成结构化数据存到了数据库里并加了全文检索。最简单的方式是存三张表会议表、发言块表、待办事项表。会议表记录会议时间、参与人、音频文件路径发言块表记录每条发言的说话人、时间戳、文本待办事项表从发言文本里用正则和规则提取。待办提取我用的是规则匹配比如包含“需要”“待办”“跟进”“本周内”“下次会议”这些词的句子抽出来作为待办候选再由人确认。规则不完美但在MVP阶段够用。用自带的文本搜索功能按说话人、按关键词过滤比翻录音文件高效太多。5. 排坑合集我踩过的那些坑和解决方案5.1 任务一直pending或者报FailedOperation提交任务之后如果长时间停在pending先别急着找客服检查两点音频时长是否超过了接口限制。录音文件识别对音频时长有上限要求超长音频要分段提交。音频格式是否在支持列表里。虽然接口支持mp3、wav、m4a等但个别非标准编码的文件会解码失败。我遇到过一次某录音笔导出的wav是特殊编码正常播放器能放识别服务解析不了。FailedOperation大概率是音频解码失败建议先用ffmpeg转一次标准格式ffmpeg -i input.wav -ar 16000 -ac 1 output_16k.wav这一步能解决大部分音频兼容问题。5.2 识别结果里的数字、英文和标点不好看会议室场景经常出现版本号、邮箱、金额。识别结果里可能把“v3.2.1”识别成“v三二一”把“5000元”识别成“五千元”。这不算识别错误但纪要么统一口径。我的处理方式是不做强制转换保留识别原文但在导出时提供“数字标准化”选项用正则把中文数字转成阿拉伯数字统一格式。如果你们团队对版本号特别敏感建议把这些字符串提前维护进热词表效果比事后清洗好。5.3 多人讨论时说话人分得很乱说话人分离不是万能的。线上会议里大家同时说话或者有人离麦克风很远都会导致说话人编号频繁跳变。我踩过最严重的一次一个半小时的会识别出九个说话人实际只有四个人参加。排查后发现是其中一个人经常打断别人声音叠在一起算法把打断前后的句子都分给了那个人。我的处理办法是会后用语义相关性做二次合并如果相邻两段的说话人编号不同但文本内容在聊同一个主题就自动合并成同一段再标注“可能存在说话人重叠”。这样虽然做不到完全准确但至少纪要看起来不碎。5.4 成本控制与限流云端API按量计费录音文件识别按识别的音频时长计费。我跑一场四十分钟的会议识别费用在几块钱量级比人工整理划算太多。但如果团队会议量很大还是建议购买资源包避免月底账单难看。限流方面录音文件识别和实时识别都有并发限制默认并发一般够个人和中小团队用。如果单场会议音频特别多要注意控制提交节奏或者申请提高配额。轮询状态的时候sleep间隔设成3秒以上别用0.5秒这种短间隔长时间轮询既没必要也容易被限流。最后说几句实在话我自己实际用下来的最大体会是语音识别只是整个工具的前半段后半段的清洗、聚合、归档才是真正花时间的地方。识别结果从“能听”到“能看”再到“好用”每一步都需要根据你们团队的说话习惯去调。热词表要持续维护说话人合并规则要跟着会议形式调整待办提取的规则也要定期补充新说法。这个工具不是一次做完就完了它是越用越聪明的。最后再分享一个小技巧如果用录音笔录音尽量让设备离说话人近一点或者用领夹麦背景噪声和混响减少之后识别率会有肉眼可见的提升。音频采集阶段做好一点后面所有环节都会轻松很多。这个项目后续我打算加上自动生成会议摘要和自动推送纪要方向已经验证过了剩下的就是一步步把体验做细。