资讯详情

资讯详情

本地AI会议纪要助手:从录音到结构化纪要的源码实现

简介Cluelessly 是一套面向开发者与 AI 应用爱好者的 AI 会议纪要助手源码聚焦会议场景下的实时转录、对话分析与行动项提取适合希望自建本地会议助理、研究 OpenAI Realtime API 落地实践的中高级开发者参考。项目采用 Laravel 12 搭配 Vue 3.5 与 TypeScript、Inertia.js 构建前后端桌面端基于 NativePHP/ElectronAI 能力由 OpenAI Realtime API 驱动样式使用 Tailwind CSS 4.1.1本地对话数据以 SQLite 存储并支持跨会话上下文保护与见解生成。压缩包共 347 个文件约 65.05MB其中 155 个 vue 组件、77 个 php 后端逻辑、41 个 ts 脚本构成主体另含 md 文档、sh 脚本、json 与 yml 配置及少量图片资源目录结构完整。已有 115 人学习关注读者可据此理解实时转录链路、动作项抽取与桌面端集成思路快速搭建可运行的会议助手原型。1. 当会议录音变成一份能直接发出去的纪要上周三下午四点我开完一场跨部门需求评审录音时长 47 分钟。按老办法我得戴上耳机重听一遍边听边敲字整理出待办、结论、争议点再排版成纪要发群里——这套流程走完一个下午就没了。后来我把这段录音丢进一套本地跑的 AI 会议纪要助手从上传到拿到结构化纪要用了不到 90 秒待办事项还自动带上了负责人和截止时间。这就是「Cluelessly - AI 会议纪要助手源码」这个方向要解决的问题把一段会议录音或转写文本自动变成一份有摘要、有决议、有待办、有发言归类的纪要。它适合三类人——每天被会议纪要追着跑的项目成员、想把纪要能力集成进自己系统的开发者、以及想拿一套可改源码做二次开发的技术团队。源码在手意味着你能改提示词、换模型、调输出格式而不是被某个闭源工具卡死。2. 会议纪要助手的技术链路从音频到结构化文本2.1 为什么是「转写 大模型抽取」两段式而不是端到端很多人第一反应是找一个能直接「音频进、纪要出」的模型。我试过翻车概率很高。原因在于会议音频里混着口音、多人抢话、专业术语直接端到端做模型既要扛声学识别又要扛语义归纳任何一环出错都会污染最终结果而且你根本不知道错在哪。稳妥的链路是拆成两段第一段做语音转文字ASR把音频变成带时间戳的文本第二段用大语言模型LLM对文本做结构化抽取。这样拆的好处是每一段都能单独验证——转写错了你去看 ASR 结果抽取错了你去调提示词。常见做法是 ASR 用本地可部署的开源方案LLM 用 API 或本地量化模型两者解耦。提示如果你的会议本身就有实时字幕或转写文本可以跳过 ASR直接把文本喂给抽取层省掉一大半算力。2.2 结构化抽取要抽哪几个字段纪要不是一段摘要就完事。真正能用的纪要字段是固定的。我一般会定义这么几个输出字段字段含义是否必填summary整体摘要35 句是decisions已达成的决议列表是todos待办事项含负责人、事项、截止时间是issues悬而未决的争议点否speakers发言人及其主要观点否字段定死之后提示词就好写了——你是在让模型填一张表而不是让它自由发挥。自由发挥的纪要十有八九会漏掉待办或者把「我们再讨论一下」当成决议。2.3 用 Python 跑通最小抽取链路下面这段代码是抽取层的核心输入是转写文本输出是 JSON 格式的纪要。我用的是通用的 OpenAI 兼容接口写法你换成任何兼容的模型端点都能跑。import json from openai import OpenAI client OpenAI( api_keyyour-api-key, # 换成你自己的 key base_urlhttps://your-endpoint/v1 # 换成兼容端点 ) # 系统提示词把模型角色锁死在「会议纪要抽取器」上 SYSTEM_PROMPT 你是一个会议纪要抽取引擎。 输入是一段会议转写文本你需要输出严格的 JSON不要输出任何解释。 JSON 结构如下 { summary: 3到5句的整体摘要, decisions: [决议1, 决议2], todos: [ {owner: 负责人, task: 待办内容, deadline: 截止时间或未提及} ], issues: [争议点1], speakers: [{name: 发言人, points: [观点1]}] } 如果某个字段在文本中没有对应内容返回空数组或空字符串。 def extract_minutes(transcript: str) - dict: resp client.chat.completions.create( modelyour-model-name, # 换成你的模型名 messages[ {role: system, content: SYSTEM_PROMPT}, {role: user, content: transcript} ], temperature0.2, # 低温度减少自由发挥 response_format{type: json_object} # 强制 JSON 输出 ) return json.loads(resp.choices[0].message.content) if __name__ __main__: text 这里放你的会议转写文本 result extract_minutes(text) print(json.dumps(result, ensure_asciiFalse, indent2))逻辑说明系统提示词负责定义输出结构temperature0.2是为了压住模型的发散倾向response_format强制 JSON 能省掉大量解析容错代码。参数上temperature别超过 0.4否则待办字段会开始编造截止时间model选长上下文版本因为一场会的转写文本动辄几千到上万字上下文不够会被截断截断位置之后的待办全丢。2.4 转写层怎么接分片与时间戳对齐如果走 ASR音频要先切片。常见做法是按静音检测切而不是按固定时长切——固定切会把一句话劈成两半转写出来就是残句。切片后每片单独转写再按时间戳拼回完整文本。拼接时保留[mm:ss]前缀方便后面定位「这句话是谁在什么时候说的」。# 伪代码示意按静音切分后逐片转写并拼接 segments split_by_silence(audio_path, min_silence_ms500) full_text [] for seg in segments: text asr_transcribe(seg.path) # 调用你的 ASR ts format_timestamp(seg.start_ms) # 转成 mm:ss full_text.append(f[{ts}] {text}) transcript \n.join(full_text)min_silence_ms这个参数很关键设太小会把正常换气切成碎片设太大则多人抢话的段落会被合并成一片发言人区分就废了。我一般从 500 毫秒起步根据实际录音质量上下调。3. 提示词工程让模型稳定吐出可用的纪要字段3.1 少样本示例比长篇规则更管用写提示词时很多人喜欢堆规则「你要注意区分决议和讨论」「待办必须带负责人」。规则写多了模型反而顾此失彼。我的血泪经验是给两个输入输出示例比写二十条规则有效。示例要覆盖边界情况。比如给一个「有人说了待办但没指定负责人」的例子让模型学会填「未指定」而不是瞎编一个人名。再给一个「讨论很激烈但没有结论」的例子让模型学会把内容放进issues而不是decisions。3.2 分步抽取先摘要再抽待办一次性让模型输出所有字段长会议下容易漏。更稳的做法是分两步第一步只出摘要和决议第二步拿着摘要和原文专门抽待办。第二步的提示词可以更聚焦「以下文本中哪些句子表达了未来要执行的动作提取负责人、动作、时间。」def extract_todos(transcript: str, summary: str) - list: prompt f已知会议摘要{summary} 请从下面的会议原文中抽取所有待办事项。 只输出 JSON 数组每项包含 owner、task、deadline。 没有明确负责人的填未指定没有明确时间的填未提及。 会议原文 {transcript} resp client.chat.completions.create( modelyour-model-name, messages[{role: user, content: prompt}], temperature0.1, response_format{type: json_object} ) return json.loads(resp.choices[0].message.content)分步的代价是多一次调用但换来的是待办召回率明显提升。我实测过一场 40 分钟的会单步抽取平均漏 23 条待办分步后基本能全捞出来。3.3 输出格式校验别信模型一定给合法 JSON即使开了response_format模型偶尔还是会吐出带 markdown 代码块包裹的 JSON或者多一个逗号。上线前一定要加一层校验和修复。import re def safe_parse(raw: str) - dict: # 去掉可能的 json 包裹 raw re.sub(r^(json)?|$, , raw.strip(), flagsre.MULTILINE) try: return json.loads(raw) except json.JSONDecodeError: # 兜底截取第一个 { 到最后一个 } start, end raw.find({), raw.rfind(}) if start ! -1 and end ! -1: return json.loads(raw[start:end1]) raise这层兜底看着土但能救回不少线上请求。校验失败时不要静默丢弃记日志方便回头调提示词。4. 避坑与排查纪要助手最容易翻车的五个地方4.1 现象待办里出现原文根本没有的人名原因模型在补全结构时看到owner字段为空会倾向于「编一个合理的」填进去。这是语言模型的通病不是你的提示词写错了。解决在提示词里明确写「没有明确负责人时填『未指定』」并在后处理里加一道校验——如果owner不在原文出现过的姓名列表里就替换成「未指定」。姓名列表可以从转写文本里用简单的正则或分词先抽一遍。4.2 现象长会议后半段的待办全丢原因转写文本超过模型上下文窗口被静默截断。很多接口不会报错直接截掉尾部。解决先估算 token 数超过窗口的按议题或时间切成多段分别抽取后再合并去重。合并时按task文本做相似度去重避免同一件事被抽两遍。4.3 现象决议和讨论混在一起原因提示词没有给「决议」下可操作的定义。模型不知道「我们倾向于方案 A」算不算决议。解决在提示词里给判定标准——「只有当出现明确的确认、拍板、通过类表述时才算决议出现『可能』『再看看』『倾向于』的一律归入 issues」。标准要具体到词不要写「根据语义判断」。4.4 现象发言人张冠李戴原因ASR 只给文本不给说话人或者说话人分离diarization做错了。多人抢话时尤其严重。解决如果录音质量允许先做说话人分离再转写如果做不到就在纪要里弱化发言人字段只保留「主要观点」别硬标名字。标错了比不标更误导人。4.5 现象同一场会跑两次结果差很多原因temperature设高了或者模型本身有随机性。纪要这种要发出去的东西结果不稳定是致命的。解决temperature压到 0.10.2能设随机种子的接口就固定种子。另外把提示词版本化每次改动都记录出问题能回滚。5. 把纪要助手用出进阶价值三个我常做的动作第一个动作是给纪要加「回链」。每一条待办和决议后面附上它在转写文本里的时间戳。这样别人看纪要时对某条结论有疑问能直接跳回原话核对。实现上就是在抽取时让模型顺带输出source_ts字段虽然会稍微增加输出长度但可信度提升明显。第二个动作是做增量纪要。会议开了一小时中途休息时先跑一次抽取拿到阶段性纪要会议结束后再跑一次全量两次结果做 diff只把新增的决议和待办推送给参会人。这样大家不用等会开完才看到东西。第三个动作是建一个「纪要质量抽检」的小流程。每周随机抽两份纪要人工核对待办召回率和决议准确率把错例收集起来反哺提示词。我坚持这么做之后提示词的迭代方向从「拍脑袋改」变成了「按错例改」效果稳定得多。验证一套纪要助手好不好用我的标准很朴素拿一场你自己开过的会把录音丢进去看输出的待办里有没有你当时真正记下来的那几条。如果漏了去看是转写漏了还是抽取漏了定位到具体环节再调。别一上来就换模型八成问题出在提示词和分片策略上。我自己踩过最深的一个坑是早期图省事把整场会的转写文本一股脑塞给模型结果待办漏了一半还以为是模型不行换了好几个模型都一样。后来才发现是上下文超了被截断改成按议题分段抽取问题当场消失。从那以后我养成了一个习惯任何跟长文本打交道的大模型链路第一步永远是先量 token再谈效果。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →