会议记录这件事,折磨过每一个需要写纪要的人。我之前在某企业服务团队做内部效率工具,每周要参加四五场例会,大部分时间都耗在会后补纪要上——重新听录音、抓重点、整理待办,一个小时的会,往往要花两三个小时收拾。后来我干脆把腾讯云AI语音识别接进来,做了一个会议小帮手:开会时录音,会后自动转成带说话人标记的逐字稿,再整理成带议题、结论和行动项的会议纪要。整套流程,从录音结束到拿到第一版纪要初稿,基本控制在几分钟内。这篇文章把完整的技术方案、核心接入代码、踩过的坑和调优经验整理出来,给同样被会议纪要折磨的人一个可复用的参考。
会议小帮手适合谁?如果你跟我一样需要频繁产出会议记录,手头又有一批散落在手机、录音笔、会议平台上的音频文件,或者正打算给团队做一个轻量的会议记录工具,这篇内容基本可以当作一份开发指南来用。我会把每一步“为什么这么选”讲清楚,因为只有理解了选型和取舍,你才能在复制方案之后,真正按自己的场景去调整。
1. 项目定位:先想清楚要做一个什么样的“会议小帮手”
1.1 会议记录场景里的真实麻烦
开会这件事,产出纪要的痛点不在“记录”本身,而在“回听”和“整理”。我观察过身边不少同事,开完会之后陷入同一个循环:先翻手机录音,找到对应段落重新听,再手动把关键信息敲进文档。遇到头脑风暴型的会,一句话要反复听两三遍才能确定谁说的、说的是什么。这个过程的隐性成本非常夸张——一场一小时的会议,整理纪要通常要花两小时以上,而且完全无法复用,下次开会重新来一遍。
市面上不是没有工具,会议平台自带的转写功能确实能用,但问题在于它只认自己平台上的录屏和音轨。我们团队平时开会,一半是内部视频会议,一半是线下讨论,还有客户现场沟通用手机录音,文件格式五花八门。这种情况下,真正合适的产品形态不是“再装一个会议软件”,而是一个独立于会议平台之外的转写管线:你给我任意一段音频,我还你一份带说话人的结构化纪要。
1.2 功能边界:小帮手不自作主张
很多人在做类似工具时容易犯一个毛病:功能越加越多,最后变成四不像。我在这版工具上刻意做了减法,只保留五个核心能力:
- 支持上传录音文件并自动转写,兼容常见的 m4a、mp3、wav 格式。
- 转写结果带说话人分离,能区分“谁在什么时候说了什么”。
- 自动生成结构化纪要,包含议题摘要、关键结论、行动项三块。
- 纪要支持人工编辑、导出 Markdown 和 Word,因为纯自动的结果总会有漏网之鱼。
- 支持历史纪要全文检索,方便回头翻旧账。
明确不做的事:不做实时通话降噪、不做多语言实时翻译、不做会议日程管理。实时转写我留了一个口子,但只作为扩展能力接入,MVP 阶段以“会后处理”为第一优先级。原因很简单:会后处理链路简单、稳定、成本低,而且能覆盖绝大多数使用场景。等到这条主链路跑通,再考虑实时字幕这类锦上添花的功能。
2. 技术选型:本地开源方案还是云厂商API
2.1 摆在面前的几条路
动手之前,先对比了三条路线。第一条是纯本地部署开源语音识别模型。优点是数据不出内网、长期使用没有按量计费,缺点是中文会议场景的断句、标点、说话人分离效果参差,而且实时转写对 GPU 资源有要求,团队没有现成的推理服务器就得额外投入硬件。
第二条是全自研声学模型,这个直接排除了。语音识别不是靠一两个工程师短期能追平的领域,训练数据、标注成本、迭代周期都不可控,对一个内部效率工具来说属于典型的重复造轮子。
第三条就是接云厂商的语音识别API。腾讯云AI语音识别是这条路线里我最终选定的方案。原因很直接:中文普通话在真实会议场景下的识别表现足够好,原生支持说话人分离和标点恢复,还提供热词表机制可以针对人名、产品名做定向纠偏。接入成本低,官方 SDK 覆盖主流语言,新用户有免费额度,前期验证几乎不花钱。对一个小团队内部工具来说,性能够用、接入快、按量付费,比自维护一套推理集群划算得多。
2.2 为什么是“录音文件识别”而不是“实时识别”
腾讯云的语音识别接口按使用场景分了好几类,我们这个项目主要卡在两类之间:录音文件识别和实时语音识别。
录音文件识别是异步接口,提交音频后返回一个任务ID,服务端排队识别,客户端轮询状态拿结果。适合整场会议录音这种“事后处理”场景,支持长时间音频,结果里带完整的时间戳和说话人信息。
实时语音识别走 WebSocket,音频流边发边识别,返回临时结果和最终结果,适合会议现场实时显示字幕。缺点是连接维持成本高,网络不稳时容易断线,而且长时间挂着的费用比一次性识别要高。
我最后选了录音文件识别作为主路径,实时识别只做了预研。核心考虑是使用场景:会议纪要通常是在会后整理,不需要实时字幕。录音文件识别的任务模型天然适合并发——上午连着传三场会的录音,系统排队处理,我喝杯咖啡回来就全好了。
2.3 整体架构预览
工具采用前后端分离的轻量架构。前端是一个网页,负责上传音频、展示转写进度、呈现逐字稿和纪要、提供编辑与导出功能。后端用 Python 写成,对外提供 REST API,内部维护一个任务状态机,负责音频预处理、调用腾讯云识别接口、轮询任务状态、后处理文本、生成纪要。
音频文件的临时存储放在本地磁盘,生产环境可以替换成对象存储。数据库用 SQLite 起步,表结构非常简单:一张会议表存元数据,一张转写明细表存每个句子的说话人和时间戳,一张纪要表存最终结果。整个项目从零到能跑,我一个人一周左右就完成了,这个体量很适合作为团队内部工具或者个人练手项目。
3. 环境准备与核心接入:腾讯云ASR接口实战
3.1 开通服务与密钥管理
第一步是开通语音识别服务。登录腾讯云控制台,在产品列表里找到语音识别,按引导开通即可。真正需要留意的是密钥管理:不要用主账号的密钥直接调接口,而是进入访问管理模块,创建一个只授语音识别相关权限的子账号,拿到独立的 SecretId 和 SecretKey。这样做的好处是权限可控,即使密钥泄露也不会波及云服务器、数据库等核心资源。
密钥拿到之后,项目里不能硬编码。我的做法是放到环境变量里,本地开发用.env文件,部署时用密钥管理服务注入。代码里通过os.environ.get()读取,避免把密钥提交到仓库。这个小习惯能省掉很多不必要的麻烦。
3.2 录音文件识别的完整接入代码
SDK 安装很简单,官方 Python SDK 一条命令搞定:
pip install tencentcloud-sdk-python我封装了一个识别模块,核心是两个函数:一个创建识别任务,一个轮询任务状态。创建任务的请求里,关键参数是引擎模型、音频来源、声道数、说话人分离开关和预估人数。
import os import time 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 = os.environ.get("TENCENTCLOUD_SECRET_ID") SECRET_KEY = os.environ.get("TENCENTCLOUD_SECRET_KEY") REGION = "ap-guangzhou" def get_client(): cred = credential.Credential(SECRET_ID, SECRET_KEY) http_profile = HttpProfile() http_profile.endpoint = "asr.tencentcloudapi.com" client_profile = ClientProfile() client_profile.httpProfile = http_profile return asr_client.AsrClient(cred, REGION, client_profile) def create_rec_task(audio_url: str, speaker_numbers: int = 4) -> int: client = get_client() req = models.CreateRecTaskRequest() req.EngineModelType = "16k_zh" req.SourceType = 0 req.Url = audio_url req.ChannelNum = 1 req.SpeakerDiarization = True req.SpeakerNumbers = speaker_numbers req.FilterDirty = 1 resp = client.CreateRecTask(req) return resp.TaskId def wait_for_result(task_id: int, interval: int = 3): client = get_client() req = models.DescribeTaskStatusRequest() req.TaskId = task_id while True: resp = client.DescribeTaskStatus(req) if resp.Status == 2: return json.loads(resp.Result) if resp.Status == 3: raise RuntimeError(f"识别失败: {resp.ErrorMsg}") time.sleep(interval)任务状态码的含义很简单:0 表示排队中,1 表示识别中,2 表示识别完成,3 表示识别失败。轮询间隔我设成 3 秒,对内部工具来说足够及时,也不会频繁打爆接口配额。
识别完成的 Result 是一个 JSON 字符串,里面最重要的字段是 SentenceList,每一项代表一个识别出的句子,包含正文、开始时间、结束时间和说话人ID。解析代码在下一节一起讲。
3.3 实时识别链路预留
虽然 MVP 以录音文件识别为主,我还是把实时识别的接入要点梳理出来了,方便后续扩展。实时识别的协议是 WebSocket,连接地址需要自己拼签名参数。流程是:先构造带签名信息的 URL,建立连接后发送一个 start 类型的 JSON 帧,然后持续发送音频二进制帧,同时接收识别结果文本帧。
import os import json import hmac import hashlib import base64 import time import websockets def build_ws_url(): secret_id = os.environ.get("TENCENTCLOUD_SECRET_ID") secret_key = os.environ.get("TENCENTCLOUD_SECRET_KEY") now = int(time.time()) expired = now + 300 path = f"secretid={secret_id}×tamp={now}&expired={expired}&voice_format=1&engine_model_type=16k_zh" sign_str = f"POST%2Fasr%2Fopen%2Fv2%3F{path}" signature = base64.b64encode( hmac.new(secret_key.encode(), sign_str.encode(), hashlib.sha1).digest() ).decode() return f"wss://asr.cloud.tencent.com/asr/open/v2?{path}&signature={signature}" async def realtime_transcribe(audio_chunks): url = build_ws_url() async with websockets.connect(url) as ws: await ws.send(json.dumps({ "type": "start", "voice_format": 1, "engine_model_type": "16k_zh", "need_vad": 1 })) for chunk in audio_chunks: await ws.send(chunk) async for message in ws: data = json.loads(message) if data.get("type") == "result": print(data.get("text", ""))这里只展示核心骨架,实际生产环境需要处理重连、断线续传、音频分片大小控制。签名算法的细节以官方文档为准,不同版本协议会有参数差异。我给这个模块预留了接口,等项目二期需要做现场字幕时再完善。
4. 会议纪要生产链路:从音频到结构化正文
4.1 音频预处理:采样率和声道标准化
腾讯云ASR对音频格式有明确要求,最省心的输入是 16kHz 采样率、16bit 位深、单声道的 PCM/WAV 数据。而手机录音基本是 48kHz 立体声 m4a,会议平台导出的是各种压缩格式,直接提交识别也能处理,但效果和稳定性不如标准化之后的音频。
我统一用 ffmpeg 做预处理,一条命令搞定:
ffmpeg -i 原始录音.m4a -ar 16000 -ac 1 -sample_fmt s16 预处理.wav参数含义分别是:-ar 16000重采样到 16kHz,-ac 1混音成单声道,-sample_fmt s16统一为 16bit 整数编码。这个标准化步骤能显著减少接口报错率。
有一个容易踩的坑:如果录音是双声道,且两个声道的语音内容不同,比如用双麦克风设备录的对面两个人,简单强制混音成单声道反而会让两个人声叠在一起,识别效果变差。处理之前先听一下或者看频谱,确认两个声道是否一致,再做决定。
4.2 说话人分离与角色映射
说话人分离是会议场景最实用的能力。启用后,转写结果里的每个句子都会带一个 SpeakerId,通常从 0 开始编号。我在解析层做了一层映射,把原始ID替换成可读的“说话人1、说话人2”,后续再允许用户手动改成真实姓名。
def parse_sentence_list(result_json): sentences = result_json.get("SentenceList", []) speaker_names = {} lines = [] for item in sentences: speaker_id = item.get("SpeakerId", 0) speaker_name = speaker_names.get(speaker_id, f"说话人{speaker_id + 1}") lines.append({ "speaker": speaker_name, "start": item.get("StartTime", 0), "end": item.get("EndTime", 0), "text": item.get("Text", "") }) return lines分离效果和SpeakerNumbers参数强相关,这个参数是预估的说话人数。设少了会把两个人合成一个,设多了会把一个人切成好几个碎片。我的经验是填接近实际参会人数,比如四个人开会就填 4。如果发现识别结果里说话人来回跳,优先调整这个参数,而不是去怀疑模型能力。
4.3 转写文本后处理:从逐字稿到会议纪要
逐字稿不能直接当纪要。开会的口语文本充满语气词、重复、无意义的“然后”“那个”,直接甩给参会人是灾难。我做了三道处理工序。
第一道是清洗。过滤语气词,去掉FilterDirty已经处理的脏词,再对重复字做合并。这块用正则就能搞定,不需要上模型。
第二道是结构化。按说话人聚合句子,同一个说话人连续说的话合并成一段,段头标注说话人和时间区间。这一步生成的是“带时间轴的对话流”,方便用户跳回录音核对。
第三道是提取行动项。我用的是一种混合策略:先用规则从逐字稿里捞含“负责、跟进、需要、安排、提交、确认、回复、上线”等关键词的句子,作为行动项候选;再配合大模型把候选句子归纳成简洁的待办条目。规则部分代码很简单:
ACTION_KEYWORDS = ["负责", "跟进", "需要", "安排", "提交", "确认", "回复", "上线", "截止"] def extract_action_items(lines): items = [] for line in lines: if any(keyword in line["text"] for keyword in ACTION_KEYWORDS): items.append({ "speaker": line["speaker"], "text": line["text"], "time": line["start"] }) return items大模型归纳是可选增强,不接也不影响主流程。最终纪要的排版用模板生成,输出包含基本信息、议题摘要、关键结论、行动项四部分。这套“规则保底、模型增强”的组合,在成本和效果之间找了个平衡点。
5. 会议小帮手的完整产品闭环
5.1 后端任务状态机设计
工具的后端不复杂,但任务状态管理必须严谨。因为录音文件识别是异步过程,一个会议从上传到出纪要,中间要经历预处理、提交识别、轮询、后处理多个阶段。我把状态机设计成六个状态:
uploaded -> preprocessing -> asr_queued -> asr_running -> postprocessing -> done \ / \-----> asr_failed -----/每个状态对应一个处理函数,失败状态有重试逻辑,最多重试三次。数据库里用一张meetings表记录元数据和张transcript_lines表存逐字稿,meetings表的核心字段是id、title、audio_path、status、asr_task_id、raw_result。
对外 API 设计得很简单,核心就四个:
POST /api/meetings:上传音频文件,创建会议记录。GET /api/meetings/{id}:查询状态和基本信息。GET /api/meetings/{id}/transcript:获取逐字稿。GET /api/meetings/{id}/report:获取结构化纪要。
原型阶段我用 FastAPI 的 BackgroundTasks 处理异步链路,任务少的时候完全够用。如果会议数量上来,再平滑迁移到 Celery 或者消息队列,状态机本身不受影响。
5.2 前端展示:逐字稿、编辑与导出
前端的核心是逐字稿时间线。每一行句子按说话人着色,左侧显示说话人名称,中间是文字,右侧是时间戳。用户点击时间戳,可以直接打开对应的录音片段播放,这个功能在核对识别错误时特别有用。
纪要页面提供编辑能力。自动生成的内容毕竟有误差,我保留了一个“可编辑正文区”,用户可以直接修改说话人名称、增删行动项、调整结论描述。编辑后的内容以 JSON 格式回存,下一次打开同一场会议,看到的是用户修正后的版本,而不是重新从逐字稿生成的内容。
导出功能支持 Markdown 和 Word。Markdown 用于沉淀到团队知识库,Word 则满足客户场景下的正式交付需求。Word 生成用模板填充,样式固定,减少后期排版工作。
5.3 历史纪要检索
会议纪要积累多了,检索就变成刚需。MVP 阶段我直接用 SQLite 的全文索引,查询语句很简单:
SELECT * FROM transcript_lines WHERE meeting_id IN (SELECT id FROM meetings WHERE title LIKE ?) AND text LIKE ?;因为数据量不大,这个方案响应足够快。检索范围包括会议标题、逐字稿正文、行动项内容。后续如果历史会议超过几千场,再考虑接入专业的全文检索服务,目前没必要。
6. 踩坑实录与调优经验
6.1 高频问题速查表
我把实际操作中遇到最多的问题整理成一张速查表,方便你复制方案时直接对照排查。
| 现象 | 可能原因 | 处理建议 |
|---|---|---|
| 提交识别任务后一直排队 | 音频过大或高峰期 | 检查音频时长,过长的先裁剪分段;错峰提交 |
| 转写结果为空 | 音频采样率或声道不符合要求 | 统一转成 16kHz 单声道 WAV,确认没有静音段 |
| 说话人A/B来回跳 | SpeakerNumbers设置不合理 | 按实际人数调整,通常设 3 到 5 效果较好 |
| 专业名词识别错误 | 热词表缺失 | 收集高频误识别词,创建热词表后重新提交 |
| 时间戳整体偏移 | 预处理对音频做了裁剪 | 记录裁剪偏移量,或在完整录音上识别 |
| 签名或鉴权报错 | 密钥错误、签名参数拼错 | 优先用官方SDK,检查环境变量是否正确注入 |
| 长音频导致内存暴涨 | 一次性读入整个文件 | 改用流式读取,按分片处理 |
| 会议中有两人同时说话 | 说话人分离模型天然受限 | 无法完全避免,在纪要中标记为“多人发言” |
6.2 提高识别准确率的几个实战技巧
第一个技巧是热词表一定要用起来。第一次跑完转写,我浏览了一遍结果,发现团队里的人名、产品名、项目代号几乎全军覆没。把这些错误词收集起来,创建热词表,关联到识别任务里,第二次识别同一批会议录音,准确率肉眼可见地提升。
第二个技巧是选对引擎模型。默认的16k_zh在标准普通话场景表现不错,但如果有方言口音,需要换方言模型;电话录音另有一套模型。开会的录音基本都是近距离麦克风采集,不要错用为电话场景模型,否则音质不匹配,副作用比想象中大。
第三个技巧是音频质量控制。这比任何参数调优都有效。手机平放在桌面上和用领夹麦录出来的音频,识别准确率差距非常大。给团队配几个便宜的领夹麦克风,带来的收益远超在代码层面折腾。
6.3 成本控制方案
成本是这类工具绕不开的话题。语音识别按音频时长或请求量付费,对内部工具来说,只要稍微控制一下,费用完全在可接受范围内。我做了三件事来控制成本。
第一,裁剪无用时长。会议录音的头部和尾部常有闲聊、入座、离场的噪音,这些内容识别出来后也没有纪要价值。预处理阶段用静音检测把前后空白段裁掉,只提交有效讨论部分。
第二,善用免费额度和分档模型。新开通服务通常有免费体验额度,前期测试完全够用。正式使用时,不同会议的重要程度不一样,普通周会用标准模型,重要项目评审才用效果好但价格更高的模型。
第三,对任务时长做对账。每次识别完成,把任务实际音频时长和预估费用记录到数据库,月底汇总。这样成本一旦异常上涨,能立刻定位是哪类会议、哪个环节出了问题。
6.4 隐私与合规注意
做会议记录工具,隐私问题必须前置考虑。录音前告知参会人并征得同意,这是最基本的底线。我在工具的打印页面和导出文件名里都加了一行提示:本会议记录已获参会人同意,仅用于内部存档。
音频文件和转写结果在本地存储时做了加密处理,并且设置了默认的保留期限,比如 90 天后自动清除原始音频,只保留文本纪要。这样既满足了业务需求,又降低了敏感数据长时间留存的风险。如果团队所在行业有更严格的数据安全规范,使用云API之前一定要提前评估审批,不要先斩后奏。
7. 落地效果与后续扩展
工具上线后,我拿自己参加的三场例会做了对比测试。三场会加起来约两小时,识别转写全部完成后,每场会的纪要整理时间从过去的一个多小时压缩到十分钟以内。说话人分离在四人的场景下基本准确,识别错误集中在生僻名词和语速过快的插话段落,配合热词表和人工修订后,交付质量足够用于团队周报存档。
这套方案后续还有几个明显的扩展方向。一是接入实时识别,做成会议现场投屏字幕,用 WebSocket 链路已经预留好了。二是把行动项和团队任务系统打通,会议里定下的待办自动同步成任务卡片,指派给对应负责人。三是在转录文本之上接一个问答式检索,以后问“上周项目评审会里关于接口联调是什么结论”,直接给出定位到句子的答案。
最后说一点体会。做这类工具,最容易犯的错误是一开始就想做完美,模型要最新、功能要最全、界面要最炫。实际上,语音识别模型的选择只占最终效果的一半,另一半在音频质量和后处理流程。先把“录音变逐字稿”这条链路跑通,再逐步加功能,成本可控,迭代也快。腾讯云AI语音识别的接入门槛很低,难的从来不是调接口,而是把识别结果变成真正能用的产品。这个会议小帮手版本很简单,但它替我省下的时间,已经远超当初搭它花的那一周了。