把会议录音交给 Gemini,自动生成会议纪要:摘要、已定结论、待办事项(负责人、截止日期、录音中的时间点)、未决问题,同时输出 Markdown 和 JSON。录音不需要先转文字,Gemini 直接“听”。
先看清成品
成品是
minutes.py。本文已用模拟客户端实际运行测试并通过:请求里包含音频和参会人名单,生成的 Markdown 待办表格、空列表占位、JSON 文件都正确。本文环境没有有效的 Gemini API 密钥,也没有真实会议录音,转写和总结质量没有实测。
注意:Gemini API 和 Google AI Studio 只在官方“可用地区”列表中的国家和地区提供,请在可用地区合规使用,不要用代理绕过地区限制。Gemini API 条款写明,使用免费服务时提交的内容可能被用于改进 Google 产品,不要上传含身份证号、银行卡号、他人隐私的材料。价格和免费额度以官方价格页为准。
注意:录音前要告知并征得所有参会人同意,遵守公司规定和当地关于录音的法律。涉及商业秘密的会议请先确认公司是否允许上传到外部服务。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
| 音频上传 | Files API,单个文件最大 2 GB,48 小时后自动删除 |
| 结构化纪要 | 摘要、决定、待办、未决问题 |
| 时间点定位 | 每个待办标注录音中的大致位置,方便回听核对 |
| 两种输出 | 给人看的 Markdown,给程序用的 JSON |
需要准备哪些环境?
| 项目 | 要求 | 说明 |
|---|---|---|
| Gemini API 密钥 | 在 Google AI Studio 创建 | 保存为环境变量 GEMINI_API_KEY |
| Python | 3.10 或更高 | |
| 依赖 | pip install google-genai pydantic pytest |
本文使用 google-genai 2.32.0 |
| 模型 | gemini-3.8-flash |
官方模型页 2026-10-19 更新时列为稳定版 |
| 录音 | mp3、wav、m4a 等常见格式 | 手机录音即可 |
官方音频文档说明:每秒音频约 32 个 token(1 分钟约 1,920 个),单次请求最长 9.5 小时音频。一小时会议大约 11.5 万 token,费用以价格页为准。
第一步:代码怎么写?
1. 项目结构
meeting-minutes/
├── minutes.py
├── test_minutes.py
└── recordings/
2. minutes.py
"""把会议录音交给 Gemini,生成结构化会议纪要(Markdown + JSON)。
用法:python minutes.py 会议.mp3 [--attendees 张三,李四]
"""
import argparse
import json
from pathlib import Path
from typing import List, Optional
from pydantic import BaseModel, Field
MODEL = "gemini-3.8-flash"
class Action(BaseModel):
owner: str = Field(description="负责人姓名,录音里没提到就写“待定”")
task: str = Field(description="具体要做的事")
due: Optional[str] = Field(description="截止日期,录音里明确说了才填,格式 YYYY-MM-DD")
quote_time: str = Field(description="这项任务在录音中被提到的大致时间点,格式 MM:SS")
class Minutes(BaseModel):
title: str = Field(description="会议主题,10 个字以内")
summary: str = Field(description="三到五句话的会议摘要")
decisions: List[str] = Field(description="会上已经确定的结论")
actions: List[Action]
open_questions: List[str] = Field(description="讨论了但没有结论的问题")
def prompt(attendees):
p = ("你是会议记录员。仔细听这段中文会议录音,按给定 JSON 结构整理纪要。\n"
"只记录录音里真实说过的内容,不要补充或推测;任务的负责人和截止日期必须是录音里明确提到的。")
if attendees:
p += f"\n参会人:{'、'.join(attendees)}。请用这些名字标注负责人。"
return p
def summarize(path: Path, client, attendees=()) -> Minutes:
f = client.files.upload(file=str(path))
it = client.interactions.create(
model=MODEL,
input=[{"type": "text", "text": prompt(attendees)},
{"type": "audio", "uri": f.uri, "mime_type": f.mime_type}],
response_format={"type": "text", "mime_type": "application/json", "schema": Minutes.model_json_schema()},
)
return Minutes.model_validate_json(it.output_text)
def to_markdown(m: Minutes) -> str:
lines = [f"# {m.title}", "", "## 摘要", "", m.summary, "", "## 决定", ""]
lines += [f"- {d}" for d in m.decisions] or ["- (无)"]
lines += ["", "## 待办", "", "| 负责人 | 事项 | 截止 | 录音位置 |", "| :--- | :--- | :--- | :--- |"]
lines += [f"| {a.owner} | {a.task} | {a.due or '未定'} | {a.quote_time} |" for a in m.actions]
lines += ["", "## 待讨论", ""] + ([f"- {q}" for q in m.open_questions] or ["- (无)"])
return "\n".join(lines) + "\n"
def main(argv=None, client=None):
ap = argparse.ArgumentParser()
ap.add_argument("audio")
ap.add_argument("--attendees", default="")
a = ap.parse_args(argv)
if client is None:
from google import genai
client = genai.Client()
names = [x for x in a.attendees.split(",") if x]
m = summarize(Path(a.audio), client, names)
stem = Path(a.audio).with_suffix("")
Path(f"{stem}-纪要.md").write_text(to_markdown(m), encoding="utf-8")
Path(f"{stem}-纪要.json").write_text(m.model_dump_json(indent=2), encoding="utf-8")
print(f"已生成 {stem}-纪要.md,待办 {len(m.actions)} 项")
return m
if __name__ == "__main__":
main()
| 要点 | 说明 |
|---|---|
{"type": "audio", ...} |
官方音频文档的输入写法 |
quote_time |
让模型给出时间点,方便你回听确认 |
| “只记录真实说过的” | 减少模型自行补充 |
| 参会人名单 | 帮助模型把“老张”对应到正确的人 |
第二步:怎样测试和运行?
1. 离线测试
import json
from types import SimpleNamespace
from minutes import main
SAMPLE = {"title": "官网改版周会", "summary": "确认了上线时间。", "decisions": ["11 月 1 日上线"],
"actions": [{"owner": "张三", "task": "完成首页设计稿", "due": "2026-10-25", "quote_time": "03:12"},
{"owner": "待定", "task": "联系 CDN 供应商", "due": None, "quote_time": "15:40"}],
"open_questions": []}
class Fake:
def __init__(self):
self.calls = []
self.files = SimpleNamespace(upload=lambda file: SimpleNamespace(uri="files/x", mime_type="audio/mpeg"))
self.interactions = SimpleNamespace(create=lambda **kw: self.calls.append(kw) or SimpleNamespace(output_text=json.dumps(SAMPLE, ensure_ascii=False)))
def test_outputs(tmp_path):
audio = tmp_path / "周会.mp3"
audio.write_bytes(b"")
c = Fake()
m = main([str(audio), "--attendees", "张三,李四"], client=c)
assert c.calls[0]["input"][1]["type"] == "audio"
assert "张三、李四" in c.calls[0]["input"][0]["text"]
md = (tmp_path / "周会-纪要.md").read_text(encoding="utf-8")
assert "| 待定 | 联系 CDN 供应商 | 未定 | 15:40 |" in md
assert "## 待讨论\n\n- (无)" in md
assert json.loads((tmp_path / "周会-纪要.json").read_text(encoding="utf-8"))["actions"][0]["owner"] == "张三"
assert len(m.actions) == 2
python -m pytest -q
本文实际输出 1 passed。
2. 用真实录音运行
export GEMINI_API_KEY="你的密钥"
python minutes.py recordings/周会.mp3 --attendees 张三,李四,王五
期望输出 已生成 recordings/周会-纪要.md,待办 N 项。生成的 Markdown 结构如下(内容取决于录音):
# 官网改版周会
## 摘要
……
## 待办
| 负责人 | 事项 | 截止 | 录音位置 |
| :--- | :--- | :--- | :--- |
| 张三 | 完成首页设计稿 | 2026-10-25 | 03:12 |
3. 核对
| 检查项 | 做法 |
|---|---|
| 待办是否真实 | 按“录音位置”回听几条 |
| 负责人是否正确 | 对照参会人名单 |
| 截止日期 | 只有会上明确说了才应出现 |
纪要发出前一定要人工过一遍。
出问题时该怎么调?
1. 人名识别错
传入 --attendees;口音重或多人同时说话时效果会下降,尽量用靠近说话人的麦克风。
2. 时间点不准
模型给出的时间点是大致位置,可能有偏差,用来辅助定位,不作为精确时间戳。
3. 上传很慢或失败
先把录音压缩成单声道 mp3:
ffmpeg -i 会议.m4a -ac 1 -b:a 64k 会议.mp3
常见问题怎么处理?
| 现象 | 原因 | 解决方法 |
|---|---|---|
| 纪要太笼统 | 提示词太宽泛 | 在提示词里说明你关心的重点 |
| 编造了没说过的待办 | 模型幻觉 | 按时间点回听,删除不实内容 |
| 文件超过 2 GB | Files API 单文件上限 | 压缩或分段 |
| 想要逐字稿 | 本脚本只做纪要 | 另写提示词要求输出带时间戳的转写 |
User location is not supported |
地区不可用 | 只能在可用地区使用 |
项目资源
建议先用一段 5 分钟左右、你自己录的测试对话跑一次,对照录音检查每一条待办。
确认效果可靠后,再用于真实会议,并且每次发出前人工审核。