基于Whisper与LLM的本地化AI会议纪要生成系统实践
2026/9/5 12:56:55 网站建设 项目流程

简介:Cluelessly 是一款开源的 AI 会议纪要助手,面向开发者、远程协作团队及效率工具爱好者,解决会议记录耗时、关键信息遗漏、行动项追踪困难等实际痛点。资源包含347个文件,以155个 Vue 组件(前端交互与界面)、77个 PHP 文件(Laravel 后端逻辑)、41个 TypeScript 类型定义与业务逻辑为主干,辅以 Markdown 文档、Shell 脚本、SQLite 数据库及 Electron 桌面集成相关文件,完整覆盖从实时语音转录、AI 分析到本地存储与桌面部署的全链路实现。压缩包大小为65.05MB,结构清晰,含 .env.example、artisan 命令行工具、Tailwind 样式配置及 OpenAI Realtime API 集成示例,便于二次开发与本地调试。目前已有111人学习下载,读者可直接获取可运行的跨平台会议辅助系统源码,掌握 Laravel+Vue+Electron 多端协同架构、AI 实时流处理实践及 SQLite 本地会话持久化方案。

1. 项目缘起:从“会议纪要”这个痛点说起

如果你和我一样,经常需要参加各种线上会议,无论是内部项目同步、客户需求沟通还是技术方案评审,那你一定对“会后整理纪要”这件事深恶痛绝。会议开完,人已经筋疲力尽,但更痛苦的任务才刚刚开始:你需要回放一两个小时的录音,反复拖动进度条,试图从一堆“嗯”、“啊”、“这个嘛”的口水话里,提炼出关键决策、待办事项和核心结论。这个过程不仅耗时,而且极其枯燥,常常一拖再拖,最后纪要变成了“回忆录”,细节早已模糊不清。

更糟糕的是,当团队规模扩大,会议频率增加,这种手动整理的方式完全不可持续。我曾经尝试过市面上的一些“智能”工具,但它们要么是简单的语音转文字,把口语化的混乱对话原封不动地扔给你,让你自己去“淘金”;要么就是功能臃肿、价格昂贵,还涉及到数据隐私的担忧。我需要的是一个能真正理解会议内容、能自动结构化输出、并且完全由我自己掌控的工具。这就是我动手开发Cluelessly的初衷——一个轻量、开源、可定制的 AI 会议纪要助手。它不只是一个转录工具,而是一个能帮你“理解”会议,并生成可直接分发的行动清单和总结报告的智能助手。

“Cluelessly”这个名字,本身带有“一无所知”或“漫不经心”的戏谑意味,恰恰反衬了它的目标:让你无需再为会议纪要的琐事费心(be clueless about meeting notes),从而能更专注于会议本身的内容和讨论。这个项目完全开源,意味着你可以查看每一行代码,根据自己的需求进行修改,甚至部署在自己的服务器上,确保所有会议数据都在你的掌控之中。

2. 核心架构设计:如何让AI“听懂”并“总结”会议

一个高效的会议纪要助手,绝不仅仅是语音转文字(ASR)那么简单。它需要完成一个从“物理信号”到“结构化知识”的完整认知链条。Cluelessly 的架构就是围绕这个链条设计的,主要分为四个核心环节。

2.1 音频处理与高精度语音转写

这是所有工作的基础。如果转写的文字错误百出,后续的分析就是空中楼阁。我们的首要目标是获取一份尽可能准确的会议文字稿。

技术选型与理由:市面上开源的ASR方案很多,如 Whisper、Vosk 等。我最终选择了OpenAI 的 Whisper模型。原因有三:第一,它的准确率在开源模型中公认最高,尤其是在中英文混合、带口音或背景噪音的场景下,表现非常稳健。第二,它支持“大模型本地部署”,虽然模型文件较大(例如large-v3模型约3GB),但一旦部署,所有计算都在本地完成,无需调用任何外部API,彻底解决了数据隐私和网络延迟的问题。第三,Whisper 不仅输出文字,还能带时间戳(Timestamp),这为后续的“谁在什么时候说了什么”的发言者区分提供了可能。

实操细节与避坑:在本地部署 Whisper 时,第一个坑就是性能。直接用CPU跑一个两小时的会议音频,可能会让你等到怀疑人生。必须使用 GPU 加速。在配置环境时,确保你的 PyTorch 版本是支持 CUDA 的。安装命令大概是这样的:

pip install openai-whisper pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whisper/cu118

使用起来非常简单:

import whisper model = whisper.load_model(“large-v3”) # 根据你的硬件选择模型大小,如 `base`, `small`, `medium` result = model.transcribe(“meeting_audio.mp3”) text = result[“text”] segments = result[“segments”] # 包含时间戳的段落列表

这里的关键是segments。它返回一个列表,每个元素包含start,end,text。这就是我们后续进行发言者分离和内容分析的原材料。

注意:Whisper 的large-v3模型效果最好,但对显存要求也高(约10GB)。如果硬件受限,可以降级到mediumsmall模型,在准确率和速度之间取得平衡。实测中,medium模型在大多数场景下已经足够可用。

2.2 发言者分离:区分“谁说了什么”

只有一份完整的文字稿还不够。一份合格的会议纪要需要知道“这句话是谁说的”,特别是当需要明确任务负责人时。这就是发言者分离(Speaker Diarization)要解决的问题。

方案选择:纯粹的声纹聚类(如 pyannote.audio)效果很好,但配置复杂,且对音频质量(如每人有独立麦克风)要求较高。对于更常见的、单声道混合了所有人声音的会议录音(比如腾讯会议导出的音频),一个更简单实用的方法是基于时间戳和文本内容的启发式规则

我们的实现逻辑:

  1. 静默检测分割:首先,我们利用segments中的时间戳。Whisper 本身会在说话间隙进行分割。我们可以设定一个阈值(比如静默超过1.5秒),认为这是一个话轮(turn)的结束。
  2. 文本内容聚类:然后,对每个话轮的文本,使用嵌入模型(例如 Sentence-BERT)将其转换为向量。
  3. 简单聚类:最后,对这些向量进行简单的聚类(如 K-Means,但K值需要预估说话人数)。聚类后,同一类的段落就被认为是同一个发言者。

这种方法虽然不是100%准确,但在3-5人的会议中,结合上下文,正确率可以接受。代码逻辑示例如下:

from sentence_transformers import SentenceTransformer from sklearn.cluster import KMeans import numpy as np # 假设 `turn_texts` 是分割后的话轮文本列表 embedder = SentenceTransformer(‘paraphrase-MiniLM-L6-v2’) embeddings = embedder.encode(turn_texts) # 预估说话人数量,这里假设为3 num_speakers = 3 kmeans = KMeans(n_clusters=num_speakers) kmeans.fit(embeddings) labels = kmeans.labels_ # 每个话轮对应的说话人标签

现在,我们就有了一个结构化的列表:[(start_time, end_time, speaker_id, text), …]

2.3 语义理解与关键信息抽取

这是AI发挥核心价值的阶段。我们需要让模型理解这些文字,并提取出我们关心的要素。传统的关键词提取或规则匹配在这里力不从心,我们需要大语言模型(LLM)的语义理解能力。

为什么用LLM而不是规则?会议讨论是自由且多样的。待办事项可能以“小明,你下周把方案发出来”的形式出现,也可能以“我们需要解决登录超时的问题”这种集体任务的形式出现。决策点可能隐藏在漫长的讨论中。LLM的优势在于它能理解上下文和意图。

Prompt工程是关键:我们不是简单地把全文扔给LLM说“总结一下”,而是通过精心设计的提示词(Prompt),引导它进行结构化输出。Cluelessly 的核心Prompt大致如下:

你是一个专业的会议纪要助手。请分析以下会议转录文本,并严格按JSON格式输出。 会议参与人:[从发言者分离结果中提取的姓名或代号列表] 会议文本:[按时间顺序排列的,带发言人的文本] 请从文本中提取并总结以下信息: 1. **会议主题:** 用一句话概括。 2. **核心讨论点:** 列出3-5个主要讨论的话题。 3. **关键决策:** 列出会议中做出的所有明确决定。 4. **待办事项(Action Items):** 列出所有明确的任务。每一项必须包含:任务内容、负责人(从参与人列表中匹配)、截止时间(如果提及)。 5. **后续计划:** 下一步的整体安排或会议安排。 请确保所有信息均来源于提供的文本,不要捏造。对于负责人不明确的任务,负责人字段设为“待定”。

本地大模型部署:为了隐私和成本,我们同样在本地部署LLM。可以选择像ChatGLM3-6BQwen-7BLlama 3这类开源模型,并使用OllamavLLM等框架进行本地服务化部署。调用方式就变成了向本地API发送上述Prompt。

import requests import json def summarize_with_llm(structured_text): prompt = f”…“ # 将上面的提示词模板化,填入具体内容 url = “http://localhost:11434/api/generate” # Ollama 默认地址 payload = { “model”: “qwen:7b”, # 你本地部署的模型名 “prompt”: prompt, “stream”: False } response = requests.post(url, json=payload) result = json.loads(response.text)[“response”] # 解析 result 中的 JSON 部分 summary = json.loads(result) # 这里需要处理LLM返回的不稳定格式 return summary

实操心得:LLM的返回格式可能不稳定,有时会在JSON外包裹额外解释文字。一个稳健的做法是,在Prompt中要求它首先输出“<JSON>”,结束时输出“</JSON>”,然后在代码中用正则表达式提取这两个标签之间的内容进行解析。

2.4 结构化输出与集成

最后一步,将LLM输出的JSON数据,渲染成人类可读的、格式优美的会议纪要。我们可以选择输出为Markdown文件,它轻便、易读,并且能很好地兼容任务列表等格式。

Markdown模板示例:

# 会议纪要 - **时间:** 2023-10-27 14:00-15:30 - **主题:** {{会议主题}} - **参会人:** 张三,李四,王五 ## 核心讨论点 1. {{讨论点1}} 2. {{讨论点2}} ## 关键决策 - [ ] 决策一:{{决策内容}} - [ ] 决策二:{{决策内容}} ## 待办事项 (Action Items) | 任务内容 | 负责人 | 截止时间 | 状态 | | :--- | :--- | :--- | :--- | | {{任务1}} | 张三 | 2023-11-03 | 待开始 | | {{任务2}} | 李四 | 2023-11-01 | 进行中 | ## 后续计划 - {{计划内容}}

我们可以使用像Jinja2这样的模板引擎,将JSON数据填充到模板中,生成最终的meeting_summary.md文件。这个文件可以直接分享到团队群,或者导入到Notion、飞书等协作平台。

3. 从源码到可运行应用:工程化实践

有了核心模块,我们需要一个“胶水”把它们粘合起来,并处理各种边缘情况,形成一个用户可以方便使用的应用。

3.1 项目结构与依赖管理

一个清晰的项目结构是长期维护的基础。Cluelessly 的目录结构大致如下:

cluelessly/ ├── cli.py # 命令行入口点 ├── config.yaml # 配置文件(模型路径、API地址、参数等) ├── requirements.txt # Python依赖 ├── src/ │ ├── audio/ │ │ ├── transcribe.py # Whisper 转写模块 │ │ └── diarization.py # 发言者分离模块 │ ├── analysis/ │ │ └── summarizer.py # LLM 总结模块 │ ├── output/ │ │ └── render.py # 纪要渲染模块(Markdown) │ └── utils/ │ └── helpers.py # 通用工具函数 └── templates/ └── meeting_template.md.j2 # Jinja2 纪要模板

使用requirements.txt管理依赖是 Python 项目的标准做法。我们的依赖可能包括:openai-whisper,sentence-transformers,scikit-learn,ollama(或openai库如果后期切换API),jinja2,pyyaml等。

3.2 配置化与参数调优

不是所有会议都一样。有的会议技术性强,需要更多技术术语识别;有的会议是头脑风暴,需要更关注创意点。因此,将关键参数配置化非常重要。

config.yaml示例:

whisper: model_size: “medium” # small, medium, large-v3 device: “cuda” # or “cpu” language: “zh” # 可设为 None 以自动检测 diarization: silence_threshold_sec: 1.5 embedding_model: “paraphrase-MiniLM-L6-v2” num_speakers: null # 设为 null 可尝试自动估计 summarizer: llm_provider: “ollama” # 可选 “openai”, “anthropic” 等 ollama_base_url: “http://localhost:11434” ollama_model: “qwen:7b” prompt_template_path: “./prompts/summary_v1.txt” output: template_path: “./templates/meeting_template.md.j2” date_format: “%Y-%m-%d”

这样,用户无需修改代码,只需调整配置文件,就能适应不同的场景。例如,在内存较小的机器上,可以将model_size改为small;如果会议主要是中文,明确指定language: “zh”可以提高转写准确率。

3.3 构建命令行界面 (CLI)

对于开发者或技术型用户,一个命令行工具是最直接高效的交互方式。我们使用 Python 的argparse或更现代的typer库来构建CLI。

一个简单的cli.py示例:

import typer from src.audio.transcribe import transcribe_audio from src.audio.diarization import separate_speakers from src.analysis.summarizer import generate_summary from src.output.render import render_markdown app = typer.Typer() @app.command() def process(audio_path: str, output_path: str = “./meeting_summary.md”): “””处理音频文件并生成会议纪要。””” typer.echo(f”开始处理音频文件: {audio_path}“) # 1. 转写 text_segments = transcribe_audio(audio_path) typer.echo(“✓ 语音转写完成”) # 2. 发言者分离 turns = separate_speakers(text_segments) typer.echo(“✓ 发言者分离完成”) # 3. AI总结 summary_json = generate_summary(turns) typer.echo(“✓ AI内容分析与总结完成”) # 4. 渲染输出 render_markdown(summary_json, output_path) typer.echo(f”✓ 会议纪要已生成: {output_path}“) if __name__ == “__main__”: app()

用户只需要在终端执行python cli.py process meeting.mp3,就可以一键获得会议纪要Markdown文件。

3.4 错误处理与日志记录

一个健壮的应用必须能妥善处理异常。比如,音频文件损坏、Whisper模型下载失败、本地LLM服务未启动、网络超时等等。

关键的错误处理点:

  • 文件检查:在处理前,验证音频文件是否存在、格式是否支持。
  • 模型加载:捕获模型加载时的异常,给出清晰的指引(如“未找到Whisper模型,请运行whisper —-help查看下载指令”)。
  • LLM调用:设置请求超时,并重试机制。如果LLM返回了非JSON内容,要有降级方案(如尝试用正则提取,或提示用户重试)。
  • 日志系统:使用 Python 的logging模块,将关键步骤、警告和错误记录到文件,方便后期排查问题。日志级别可以设置为INFO用于正常流程跟踪,WARNINGERROR用于问题定位。

4. 进阶优化与踩坑实录

在实际开发和使用的过程中,我遇到了不少预料之外的问题,也做了一些优化,这些经验可能比基础功能更有价值。

4.1 处理长音频与内存优化

Whisper 处理超长音频(如4小时以上的研讨会)时,可能会遇到内存不足(OOM)的问题。因为默认它会尝试将整个音频加载到内存中进行处理。

解决方案:分块处理。Whisper 本身就支持分块转录。我们可以将长音频按固定时长(如30分钟)分割,分别转写,最后合并结果。但这里有个坑:直接合并会导致上下文断裂,影响发言者分离和LLM总结的连贯性。

我们的策略:在分块时,设置重叠区(Overlap)。例如,每30分钟一段,相邻两段重叠5分钟。这样在合并文本时,可以通过重叠部分的内容进行对齐和去重,尽可能保证语义的连贯性。在调用Whisper时,可以使用transcribe()方法的segment参数进行更细粒度的控制。

4.2 提升发言者分离的准确性

基于文本嵌入的聚类方法在说话人风格差异大时效果不错,但如果参会人说话风格相似,或者会议中穿插了大量“对”、“是的”等短句,聚类就容易出错。

优化方法一:结合声学特征。如果条件允许,可以集成pyannote.audio这样的专业声纹工具。它不依赖文本,纯粹通过声音特征进行聚类,与我们的文本聚类方法可以形成互补。我们可以设计一个投票机制,当两种方法结果不一致时,优先采用声学方法的结果,或者交给LLM结合上下文判断。

优化方法二:人名预注册。如果会议开始前就知道参会人名单,可以做一个简单的“声纹注册”。让每个人说一两句固定的话(如自我介绍),提取其声音嵌入向量。在正式会议中,将每个话轮的声音片段与这些预注册的向量进行比对,找到最相似的人。这能极大提升准确率,特别适用于定期召开的固定团队会议。

4.3 驯服不稳定的LLM输出

LLM的“幻觉”和格式不稳定性是最大的挑战之一。你要求输出JSON,它可能给你一段前面带解释的文本,或者JSON的键名用了中文引号。

Prompt工程的迭代:经过多次测试,我发现以下技巧很有效:

  1. 角色扮演与格式强调:在Prompt开头强烈明确其角色和输出格式要求。例如:“你是一个严格遵守指令的JSON生成器。你必须只输出一个合法的JSON对象,不要有任何额外的解释、前缀或后缀。”
  2. 提供输出示例:在Prompt中直接给出一个期望的JSON结构示例。LLM的模仿能力很强,看到例子后,输出格式的稳定性会大幅提高。
  3. 分步指令:对于复杂抽取,可以要求LLM先逐步思考。例如:“首先,找出所有包含任务分配的句子。然后,为每个任务提取负责人和截止时间。最后,将结果组织成如下JSON格式...”
  4. 后处理校验:在代码中,对LLM的返回结果,一定要用try…except json.JSONDecodeError包裹。如果解析失败,可以尝试用正则清洗文本,或者给用户一个友好提示,并附上原始文本让用户手动处理。

4.4 从CLI到Web界面:提升易用性

对于非技术背景的团队成员,命令行工具门槛太高。一个简单的Web界面可以极大提升工具的可用性。

轻量级实现方案:使用GradioStreamlit这类Python框架,可以在极少的代码量下构建一个交互式Web应用。核心逻辑复用,只是将CLI的输入(文件上传)和输出(结果显示和下载)换成Web组件。

一个用Gradio实现的极简界面可能只需要几十行代码:

import gradio as gr from cli import process # 导入我们之前写好的处理函数 def process_audio_interface(audio_file): # 临时保存上传的文件 temp_path = “/tmp/uploaded_audio.mp3” # … 保存文件逻辑 output_path = “/tmp/summary.md” process(temp_path, output_path) # 调用核心处理流程 # 读取生成的Markdown内容返回 with open(output_path, ‘r’, encoding=‘utf-8’) as f: content = f.read() return content demo = gr.Interface( fn=process_audio_interface, inputs=gr.Audio(type=“filepath”, label=“上传会议录音”), outputs=gr.Markdown(label=“生成的会议纪要”), title=“Cluelessly - AI会议纪要助手” ) demo.launch()

这样,团队成员只需打开浏览器,上传录音文件,稍等片刻就能看到并下载整理好的纪要,体验瞬间提升。

开发 Cluelessly 的过程,是一个典型的“用技术解决具体生活痛点”的实践。它涉及了音频处理、机器学习、自然语言处理和软件工程等多个领域。开源出来,是希望这个思路和实现能给大家提供一个起点。你可以直接使用它,更可以基于它进行改造:比如集成到你的钉钉/飞书机器人里,自动处理群里的会议录音;或者增加对视频文件(提取音频)的支持;甚至训练一个微调模型,专门用于你所在行业(如法律、医疗)的术语和纪要格式。技术的价值,最终在于它能否实实在在地提升效率,解放人力。希望 Cluelessly 能成为你会议效率工具箱里的一件得力工具。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询