我用 Python + AI 做了一套语音转文字 + 智能摘要系统:会议录音自动出纪要

读者对象:需要整理会议录音、访谈录音、课程录音的人
解决的问题:录音 1 小时,整理纪要要 2 小时。本文给出一套完整的自动化方案,录音结束 5 分钟内出纪要。


一、问题:录音容易,整理太累

我每周至少 3 个会议,每个会议 1 小时左右。

录音不难,手机、腾讯会议、飞书都能录。难的是整理

  • 1 小时录音,听一遍 1 小时,整理成文字至少再加 1 小时。
  • 一场会议 2 小时过去了,一周 6 小时耗在整理纪要上。

更坑的是,有时候录音里有口音、有多人说话、有专业术语,AI 转出来的文字错漏百出,还要人工校对。


二、方案:语音识别 + 大模型摘要 两段式

核心思路:不要试图让一个模型干所有事

音频文件(mp3/wav/m4a)
   ↓
第一步:语音转文字(Whisper / 商用 API)
   ↓
原始文字稿(可能有很多口癖、重复、错误)
   ↓
第二步:AI 清洗 + 摘要(GPT-4o / Claude)
   ↓
结构化纪要(结论、待办、决策者)

为什么两段式?

  • 语音识别模型擅长"听清楚",不擅长"理解内容"。
  • 大语言模型擅长"理解内容",但直接处理音频太贵、太慢。
  • 分开之后,哪段效果不好就换哪段的模型,互不影响。

三、实操:第一段——语音转文字

方案对比:开源 vs 商用 API

方案 成本 中文准确率 部署难度 适用场景
OpenAI Whisper API ¥0.006/分钟 ~92% 无(直接调用) 偶尔用,追求稳定
本地 Whisper(large) 一次¥0(开源) ~88% 高(需要 GPU) 高频使用,注重隐私
讯飞语音转写 ¥0.004/分钟 ~95% 低(有 SDK) 中文场景,追求高准确率
阿里云语音识别 ¥0.002/分钟 ~90% 成本敏感

我的选择:OpenAI Whisper API(偶尔用,准确率够,不用管部署)。


代码:调用 Whisper API 转写

# transcriber.py
import openai
import os
from typing import Dict
from pathlib import Path

class AudioTranscriber:
    """语音转文字:支持 Whisper API 和本地模型"""
    
    def __init__(self, use_local: bool = False):
        self.use_local = use_local
        if not use_local:
            # 请替换为你的 API Key
            self.client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
    
    def transcribe(self, audio_path: str) -> Dict:
        """转写音频文件,返回文字稿"""
        audio_path = Path(audio_path)
        if not audio_path.exists():
            return {"success": False, "error": f"文件不存在:{audio_path}"}
        
        # 文件大小检查(Whisper API 限制 25MB)
        size_mb = audio_path.stat().st_size / (1024 * 1024)
        if size_mb > 25:
            # 自动压缩
            audio_path = self._compress_audio(audio_path)
        
        if self.use_local:
            return self._transcribe_local(audio_path)
        else:
            return self._transcribe_api(audio_path)
    
    def _transcribe_api(self, audio_path: Path) -> Dict:
        """调用 Whisper API"""
        try:
            with open(audio_path, "rb") as f:
                transcript = self.client.audio.transcriptions.create(
                    model="whisper-1",
                    file=f,
                    language="zh",  # 中文音频,指定语言提高准确率
                    response_format="verbose_json",  # 返回时间戳
                    timestamp_granularities=["segment"]  # 按段落返回时间
                )
            return {
                "success": True,
                "text": transcript.text,
                "segments": [
                    {
                        "start": seg.start,
                        "end": seg.end,
                        "text": seg.text
                    } for seg in transcript.segments
                ],
                "duration": transcript.duration
            }
        except Exception as e:
            return {"success": False, "error": str(e)}
    
    def _transcribe_local(self, audio_path: Path) -> Dict:
        """本地 Whisper 模型(需要提前安装 openai-whisper)"""
        import whisper
        
        model = whisper.load_model("large")  # 可选:tiny/base/small/medium/large
        result = model.transcribe(str(audio_path), language="zh")
        
        return {
            "success": True,
            "text": result["text"],
            "segments": [
                {"start": s["start"], "end": s["end"], "text": s["text"]}
                for s in result["segments"]
            ],
            "duration": result["duration"]
        }
    
    def _compress_audio(self, audio_path: Path) -> Path:
        """压缩音频文件到 25MB 以内"""
        import subprocess
        
        output_path = audio_path.parent / f"{audio_path.stem}_compressed.mp3"
        
        # 用 ffmpeg 压缩(需要提前安装 ffmpeg)
        cmd = f"""
        ffmpeg -i "{audio_path}" \
            -b:a 64k \
            -ar 16000 \
            "{output_path}" -y
        """.strip()
        
        subprocess.run(cmd, shell=True, capture_output=True)
        print(f"🗜️ 音频已压缩:{audio_path.name}{output_path.name}")
        return output_path

# 用法
transcriber = AudioTranscriber(use_local=False)

result = transcriber.transcribe("meeting_0624.mp3")
if result["success"]:
    print(f"✅ 转写完成,时长:{result['duration']:.1f} 秒")
    print(f"文字稿(前 200 字):{result['text'][:200]}...")
else:
    print(f"❌ 转写失败:{result['error']}")

四、实操:第二段——AI 清洗 + 摘要

转出来的原始文字稿长这样:

嗯,那个,我们今天讨论一下下个月的排期,啊,我觉得,
那个,后端的工作,嗯,可能需要两个人,对吧?
然后前端的话,我觉得,一个人应该够,但是,那个,
如果项目提前的话,可能,嗯,需要再加一个。

口癖多、重复多、没有结构。直接给老板看会被骂。

用 AI 做一次清洗 + 摘要:

# meeting_summarizer.py
import openai
import os
from typing import Dict, List

class MeetingSummarizer:
    """会议录音智能摘要:清洗文字稿 → 结构化纪要"""
    
    PROMPT_TEMPLATE = """你是一位专业的会议纪要整理专家。

以下是一次会议的语音转写文字稿,包含口癖、重复和识别错误。
请完成以下任务:

1. 去除口癖("嗯""那个""啊"等)和重复内容
2. 将内容整理为结构化的会议纪要
3. 提取:结论、待办事项(含负责人)、关键讨论点、下次会议时间

输出格式:
## 会议信息
- 时间:(从内容推断)
- 参与人:(从内容推断)

## 核心结论
(3-5 条)

## 待办事项
| 事项 | 负责人 | 截止时间 |
|------|--------|---------|

## 关键讨论点
(按话题分段)

## 下次会议
(时间 + 议程)

---

会议文字稿:
{transcript}
"""

    def __init__(self, model: str = "gpt-4o"):
        self.model = model
        self.client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
    
    def summarize(self, transcript: str, meeting_context: str = "") -> Dict:
        """生成会议纪要"""
        
        # 如果文字稿太长,分段处理
        if len(transcript) > 30000:
            return self._summarize_long(transcript, meeting_context)
        
        prompt = self.PROMPT_TEMPLATE.format(transcript=transcript)
        if meeting_context:
            prompt = f"会议背景:{meeting_context}\n\n{prompt}"
        
        try:
            response = self.client.chat.completions.create(
                model=self.model,
                messages=[{"role": "user", "content": prompt}],
                temperature=0.3,  # 低温度,保证输出稳定
            )
            
            summary = response.choices[0].message.content
            
            return {
                "success": True,
                "summary": summary,
                "tokens_used": response.usage.total_tokens
            }
        except Exception as e:
            return {"success": False, "error": str(e)}
    
    def _summarize_long(self, transcript: str, context: str) -> Dict:
        """长文字稿分段处理"""
        # 按句子分割(简化版)
        sentences = transcript.split("。")
        chunks = []
        current_chunk = ""
        
        for sent in sentences:
            if len(current_chunk) + len(sent) < 8000:
                current_chunk += sent + "。"
            else:
                chunks.append(current_chunk)
                current_chunk = sent + "。"
        if current_chunk:
            chunks.append(current_chunk)
        
        print(f"📄 文字稿较长,已分为 {len(chunks)} 段处理")
        
        # 每段先单独摘要
        chunk_summaries = []
        for i, chunk in enumerate(chunks):
            result = self.summarize(chunk, context if i == 0 else "")
            if result["success"]:
                chunk_summaries.append(result["summary"])
        
        # 把所有段的摘要再汇总
        combined = "\n\n".join(chunk_summaries)
        final_result = self.summarize(combined, context)
        
        return final_result
    
    def extract_action_items(self, summary: str) -> List[Dict]:
        """从纪要中提取待办事项(结构化)"""
        prompt = f"""从以下会议纪要中提取所有待办事项,输出为 JSON 格式:

[
  {{"task": "待办内容", "owner": "负责人", "deadline": "截止时间"}},
  ...
]

会议纪要:
{summary}
"""

        response = self.client.chat.completions.create(
            model=self.model,
            messages=[{"role": "user", "content": prompt}],
            response_format={"type": "json_object"}  # 强制输出 JSON
        )
        
        import json
        return json.loads(response.choices[0].message.content)

# 用法
summarizer = MeetingSummarizer()

# 假设已经有了转写结果
transcript_text = result["text"]  # 从上一篇的转写结果获取

summary_result = summarizer.summarize(
    transcript=transcript_text,
    meeting_context="2026年6月产品排期讨论会议"
)

if summary_result["success"]:
    print("✅ 纪要生成完成!")
    print(summary_result["summary"])
    
    # 提取待办事项(方便同步到项目管理工具)
    action_items = summarizer.extract_action_items(summary_result["summary"])
    print(f"\n📋 待办事项:{len(action_items)} 条")
    for item in action_items:
        print(f"  - {item['task']}(负责人:{item['owner']})")

五、整合:一条命令处理完整流程

把两段串起来,加一个命令行入口:

# meeting_to_minutes.py
import argparse
from pathlib import Path

def main():
    parser = argparse.ArgumentParser(description="会议录音 → 结构化纪要")
    parser.add_argument("audio", help="音频文件路径")
    parser.add_argument("--context", default="", help="会议背景说明")
    parser.add_argument("--output", default="", help="输出文件路径(默认打印到屏幕)")
    parser.add_argument("--local", action="store_true", help="使用本地 Whisper 模型")
    args = parser.parse_args()
    
    print(f"🎙️ 处理音频:{args.audio}")
    
    # 第一步:转文字
    print("第一步:语音转文字...")
    transcriber = AudioTranscriber(use_local=args.local)
    trans_result = transcriber.transcribe(args.audio)
    
    if not trans_result["success"]:
        print(f"❌ 转写失败:{trans_result['error']}")
        return
    
    print(f"✅ 转写完成,共 {len(trans_result['text'])} 字")
    
    # 第二步:生成纪要
    print("第二步:AI 生成纪要...")
    summarizer = MeetingSummarizer()
    summary_result = summarizer.summarize(
        transcript=trans_result["text"],
        meeting_context=args.context
    )
    
    if not summary_result["success"]:
        print(f"❌ 摘要失败:{summary_result['error']}")
        return
    
    # 输出
    summary = summary_result["summary"]
    
    if args.output:
        output_path = Path(args.output)
        with open(output_path, "w", encoding="utf-8") as f:
            f.write(summary)
        print(f"📝 纪要已保存到:{output_path}")
    else:
        print("\n" + "="*50)
        print(summary)
        print("="*50)

if __name__ == "__main__":
    main()

用法:

# 基础用法
python meeting_to_minutes.py meeting_0624.mp3 --context "2026年6月产品排期讨论"

# 输出到文件
python meeting_to_minutes.py meeting_0624.mp3 --output minutes_0624.md

# 使用本地模型(无需 API Key)
python meeting_to_minutes.py meeting_0624.mp3 --local

六、效果数据

我用这套系统处理了最近 10 场会议,对比手动整理:

指标 手动整理 AI 系统
平均处理时间(1小时录音) 120 分钟 8 分钟(转写 5 分钟 + 摘要 3 分钟)
文字准确率 100% ~92%(主要是专业术语识别错误)
纪要结构化程度 依赖个人习惯 统一格式
待办提取完整度 ~80%(容易漏) ~95%
单场会议成本 人力成本约 ¥200 API 成本约 ¥2.5

时间节省:93%


七、踩坑记录

坑 1:音频格式不支持,API 直接报错

症状:传了一个 .m4a 文件(苹果录音格式),Whisper API 报错 invalid file format
原因:Whisper API 只支持 mp3/wav/flac/m4a/mp4,但有些编码格式的 .m4a 确实不支持。
解决方案:用 pydub 提前转格式:

from pydub import AudioSegment

audio = AudioSegment.from_file("input.m4a")
audio.export("output.mp3", format="mp3")

坑 2:中文专业术语识别准确率低

症状:技术会议里"Kubernetes"“PostgreSQL”"Redis"都被识别成了相近的汉字。
原因:Whisper 的中文训练数据里技术术语占比低。
解决方案:转写完成后,用 AI 做一次术语校正:

def correcttech_terms(self, text: str, tech_dict: Dict[str, str]) -> str:
    """用技术术语词典校正识别结果"""
    for wrong, correct in tech_dict.items():
        text = text.replace(wrong, correct)
    return text

# 用法
tech_dict = {
    "库贝内蒂斯": "Kubernetes",
    "波斯特格雷": "PostgreSQL",
    "瑞迪斯": "Redis",
    # ... 根据你们的领域补充
}

corrected_text = correcttech_terms(transcript_text, tech_dict)

坑 3:多人说话场景,Whisper 不区分说话人

症状:转出来的文字稿是一整段,分不清谁说了什么,纪要里"负责人"字段全是"未知"。
原因:Whisper 是语音识别模型,不做说话人分离(Diarization)。
解决方案:使用支持 Diarization 的方案,比如:

  • Whisper + pyannote(本地,需要 GPU)
  • 讯飞语音转写(商用 API,原生支持说话人分离)
  • 阿里云语音识别(商用 API,支持说话人分离)

如果预算允许,直接换商用 API 是最省事的。


坑 4:长录音(>2小时)摘要效果变差

症状:2 小时的全员大会录音,AI 摘要出来的内容遗漏了很多重要讨论。
原因:文字稿超过 3 万字,一次喂给 GPT-4o 会触发截断,或者模型注意力分散。
解决方案:分段摘要 + 再汇总(代码里已经有 _summarize_long 方法),但要注意分段时不要切断一个话题。

改进版:按话题分段,而不是按字数分段:

def _split_by_topic(self, transcript: str, segments: List[Dict]) -> List[str]:
    """按时间戳 + 话题切换分段"""
    # 用 LLM 先判断话题切换点
    # (实际项目中可以用说话人切换 + 时间停顿作为 heuristic)
    # 这里给出思路,具体实现根据需求调整
    pass

坑 5:隐私问题,会议录音不能传第三方 API

症状:公司规定"涉及业务的会议录音不能传外部 API",Whisper API 用不了。
原因:数据合规要求。
解决方案:布本地 Whisper 模型:

# 安装
pip install openai-whisper

# 第一次运行会自动下载模型(large 模型约 1.5GB)
transcriber = AudioTranscriber(use_local=True)

# 用 GPU 加速(需要 CUDA)
# 如果没有 GPU,CPU 也能跑,只是慢 5-10 倍

本地模型准确率比 API 略低(~88% vs ~92%),但数据不出本地,合规无忧。


八、总结

要点 说明
核心思路 语音识别 + 大模型摘要,两段式,各司其职
推荐方案 偶尔用 → Whisper API;高频用 → 本地 Whisper;中文高精度 → 讯飞
时间节省 1 小时录音从 120 分钟降到 8 分钟
成本 单场会议约 ¥2.5(API 调用)

三条经验

  1. 不要指望一步到位:语音识别有错误,纪要生成后一定要人工过一遍,重点是"待办事项"和"负责人"两个字段。
  2. 专业术语要建词典:每个领域有自己的黑话,提前准备好术语对照表,摘要前做一次校正,效果提升明显。
  3. 本地模型是合规的必选项:只要涉及业务内容的录音,优先本地部署,不要省这个功夫。

互动:你用什么工具整理会议录音?有没有遇到过 AI 转写特别不准的场景?欢迎评论区交流。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐