语音转文字在企业 AI 中的应用——会议纪要自动化的工程实践
语音转文字在企业 AI 中的应用——会议纪要自动化的工程实践
适读人群:想在企业落地语音 AI 的开发者 | 阅读时长:约 16 分钟 | 核心价值:从录音到会议纪要的全流程工程实现,避开主要坑
有次开了个三小时的产品需求评审会,十几个人参与,讨论了很多细节。会后,产品经理问谁做会议纪要。
沉默了一分钟。
最后一个实习生打开了会议录音,开始一字一字地听着转录……
我当时就说,这个事可以自动化。
用了两周时间,从零搭了一个会议纪要自动化系统,上线后在公司内部用了半年,每周处理 30-50 场会议录音。这篇文章把整个工程方案写出来。
整体架构
系统分四个阶段:
录音文件
|
v
[预处理] 音频格式转换 + 降噪 + 分段
|
v
[语音识别] Whisper 转录 + 说话人分离
|
v
[文本整理] LLM 修正识别错误 + 格式化转录文本
|
v
[纪要生成] LLM 提取议题、决策、行动项,生成结构化纪要第一步:录音预处理
会议录音来源各种各样:手机录音 APP、腾讯会议导出、电话会议录音……格式不一,质量差异也很大。
import subprocess
import os
from pathlib import Path
def preprocess_audio(input_path: str, output_dir: str) -> dict:
"""
音频预处理:格式标准化 + 降噪 + 切分
返回处理后的文件信息
"""
input_path = Path(input_path)
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
# Step 1: 转换为标准格式(16kHz, mono, WAV)
# Whisper 官方推荐格式,能获得最好效果
normalized_path = output_dir / f"{input_path.stem}_normalized.wav"
ffmpeg_cmd = [
'ffmpeg', '-i', str(input_path),
'-ar', '16000', # 采样率 16kHz
'-ac', '1', # 单声道
'-c:a', 'pcm_s16le', # 16-bit PCM
'-af', 'loudnorm', # 响度归一化
str(normalized_path),
'-y', # 覆盖已有文件
'-loglevel', 'quiet'
]
result = subprocess.run(ffmpeg_cmd, capture_output=True)
if result.returncode != 0:
raise RuntimeError(f"ffmpeg 转换失败: {result.stderr.decode()}")
# Step 2: 获取音频信息
duration = get_audio_duration(normalized_path)
# Step 3: 长录音切分(超过 30 分钟的切分处理,避免内存问题)
segments = []
if duration > 1800: # 30分钟
segments = split_audio(normalized_path, output_dir, segment_length=1200) # 每段20分钟
else:
segments = [str(normalized_path)]
return {
'original': str(input_path),
'normalized': str(normalized_path),
'duration_seconds': duration,
'segments': segments
}
def get_audio_duration(audio_path: str) -> float:
"""获取音频时长(秒)"""
cmd = [
'ffprobe', '-v', 'quiet',
'-show_entries', 'format=duration',
'-of', 'csv=p=0',
str(audio_path)
]
result = subprocess.run(cmd, capture_output=True, text=True)
return float(result.stdout.strip())
def split_audio(input_path: str, output_dir: Path, segment_length: int = 1200) -> list[str]:
"""将长音频切分为多段"""
duration = get_audio_duration(input_path)
segments = []
start = 0
seg_idx = 0
while start < duration:
end = min(start + segment_length, duration)
seg_path = output_dir / f"segment_{seg_idx:03d}.wav"
cmd = [
'ffmpeg', '-i', str(input_path),
'-ss', str(start),
'-t', str(end - start),
str(seg_path),
'-y', '-loglevel', 'quiet'
]
subprocess.run(cmd, capture_output=True)
segments.append(str(seg_path))
start += segment_length
seg_idx += 1
return segments第二步:语音识别
Whisper 是目前最好用的开源语音识别方案,中英文效果都不错。
import whisper
import json
from typing import Optional
def transcribe_audio(
audio_path: str,
language: str = 'zh',
model_size: str = 'large-v3'
) -> dict:
"""
用 Whisper 转录音频
model_size 选择:
- tiny/base/small: 速度快,准确率低,适合测试
- medium: 平衡选择
- large-v3: 最高准确率,需要 GPU 或等待时间长
"""
model = whisper.load_model(model_size)
result = model.transcribe(
audio_path,
language=language,
verbose=False,
# 以下参数对会议录音有帮助
condition_on_previous_text=True, # 上下文连贯性
compression_ratio_threshold=2.4,
no_speech_threshold=0.6,
)
return {
'text': result['text'],
'segments': result['segments'], # 带时间戳的分段
'language': result['language']
}
def transcribe_with_timestamps(audio_segments: list[str], language: str = 'zh') -> list[dict]:
"""
处理多个音频片段,保留时间戳
用于长录音的分段处理
"""
model = whisper.load_model('large-v3')
all_segments = []
time_offset = 0 # 累计时间偏移
for seg_idx, seg_path in enumerate(audio_segments):
print(f"处理第 {seg_idx + 1}/{len(audio_segments)} 段...")
result = model.transcribe(seg_path, language=language, verbose=False)
# 调整时间戳(加上偏移量)
for seg in result['segments']:
adjusted_seg = seg.copy()
adjusted_seg['start'] += time_offset
adjusted_seg['end'] += time_offset
all_segments.append(adjusted_seg)
time_offset += get_audio_duration(seg_path)
return all_segments说话人分离(谁说了什么)
纯转录只能知道说了什么,不知道是谁说的。会议纪要里需要知道"是老王还是小李提出了这个需求"。
# 使用 pyannote.audio 做说话人分离
# pip install pyannote.audio
from pyannote.audio import Pipeline
def diarize_speakers(audio_path: str, num_speakers: Optional[int] = None) -> list[dict]:
"""
说话人分离
num_speakers: 如果已知参会人数,提供这个参数可以提高准确率
需要 HuggingFace token(pyannote 是免费使用但需要申请访问权限)
"""
pipeline = Pipeline.from_pretrained(
"pyannote/speaker-diarization-3.1",
use_auth_token="YOUR_HF_TOKEN"
)
# 执行说话人分离
kwargs = {}
if num_speakers:
kwargs['num_speakers'] = num_speakers
diarization = pipeline(audio_path, **kwargs)
# 转换为结构化格式
speaker_segments = []
for turn, _, speaker in diarization.itertracks(yield_label=True):
speaker_segments.append({
'start': turn.start,
'end': turn.end,
'speaker': speaker
})
return speaker_segments
def merge_transcription_with_diarization(
transcription_segments: list[dict],
speaker_segments: list[dict]
) -> list[dict]:
"""
将转录结果和说话人分离结果合并
为每段转录文字标注是哪位说话人
"""
merged = []
for trans_seg in transcription_segments:
seg_start = trans_seg['start']
seg_end = trans_seg['end']
# 找到时间上重叠最多的说话人
best_speaker = 'SPEAKER_UNKNOWN'
max_overlap = 0
for spk_seg in speaker_segments:
overlap_start = max(seg_start, spk_seg['start'])
overlap_end = min(seg_end, spk_seg['end'])
overlap = max(0, overlap_end - overlap_start)
if overlap > max_overlap:
max_overlap = overlap
best_speaker = spk_seg['speaker']
merged.append({
'start': seg_start,
'end': seg_end,
'speaker': best_speaker,
'text': trans_seg['text'].strip()
})
return merged第三步:LLM 修正识别错误
Whisper 的中文识别有一个让我头疼了很久的问题:专有名词识别错误。
公司名、产品名、人名……这些在训练数据里不常见的词,Whisper 经常识别成发音相似的常见词。比如"用友"识别成"有用","钉钉"识别成"叮叮",人名"志伟"识别成"之维"。
解法是让 LLM 做一遍后处理:
def fix_transcription_errors(
raw_transcription: str,
glossary: list[str] = None,
context: str = ''
) -> str:
"""
用 LLM 修正转录错误
glossary: 专有名词词表(公司名、产品名、人名等)
context: 会议背景信息
"""
client = anthropic.Anthropic()
glossary_text = ''
if glossary:
glossary_text = f"\n\n专有名词列表(请优先识别这些词):\n" + '\n'.join(f"- {term}" for term in glossary)
context_text = f"\n\n会议背景:{context}" if context else ''
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=4096,
messages=[{
"role": "user",
"content": f"""以下是会议语音识别的原始文字,可能有一些识别错误需要修正。
{context_text}
{glossary_text}
修正规则:
1. 修正明显的同音字错误(如"用友"->"有用"这类)
2. 修正语法错误造成的断句问题
3. 添加适当的标点符号
4. 保留原文的内容和语气,不要改写意思
5. 不要添加任何原文没有的内容
原始转录文字:
{raw_transcription}
请输出修正后的文字(只输出修正后的内容,不要解释):"""
}]
)
return response.content[0].text第四步:生成结构化会议纪要
这是最核心的部分,也是用户感知最明显的部分。
def generate_meeting_minutes(
corrected_transcription: str,
speaker_map: dict = None, # {'SPEAKER_00': '张总', 'SPEAKER_01': '李工'}
meeting_info: dict = None # 会议基本信息
) -> str:
"""
生成结构化会议纪要
speaker_map: 说话人 ID 到真实姓名的映射
meeting_info: {'title': '产品评审会', 'date': '2024-01-15', 'attendees': ['张三', '李四']}
"""
client = anthropic.Anthropic()
# 替换说话人 ID 为真实姓名
transcript_for_llm = corrected_transcription
if speaker_map:
for speaker_id, real_name in speaker_map.items():
transcript_for_llm = transcript_for_llm.replace(speaker_id, real_name)
# 会议基本信息
meeting_header = ''
if meeting_info:
meeting_header = f"""
会议标题:{meeting_info.get('title', '未知')}
会议日期:{meeting_info.get('date', '未知')}
参会人员:{', '.join(meeting_info.get('attendees', []))}
"""
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=3000,
messages=[{
"role": "user",
"content": f"""请根据以下会议转录文字,生成一份结构化的会议纪要。
{meeting_header}
会议转录:
{transcript_for_llm}
请按以下格式生成纪要:
# 会议纪要
{meeting_header}
## 会议背景
(一句话描述本次会议的目的)
## 主要议题与讨论内容
(按讨论的主题分类,每个议题包括讨论内容和主要观点)
## 重要决定
(本次会议达成的明确决定,用列表形式)
## 待办事项
(每条包括:负责人 | 事项内容 | 截止日期/时间节点)
## 遗留问题
(未解决、需要后续跟进的问题)
注意:
- 忠实反映会议内容,不添加推断
- 遗漏或不清晰的信息可以标注"(待确认)"
- 行动项必须有明确的负责人"""
}]
)
return response.content[0].text完整的流水线
把以上步骤串起来:
import os
from datetime import datetime
def process_meeting_recording(
audio_path: str,
output_dir: str,
meeting_info: dict = None,
glossary: list[str] = None,
speaker_map: dict = None,
num_speakers: int = None
) -> dict:
"""
会议录音处理完整流水线
"""
os.makedirs(output_dir, exist_ok=True)
print("Step 1: 音频预处理...")
audio_info = preprocess_audio(audio_path, f"{output_dir}/audio")
print("Step 2: 语音识别...")
all_segments = transcribe_with_timestamps(
audio_segments=audio_info['segments'],
language='zh'
)
print("Step 3: 说话人分离...")
try:
speaker_segments = diarize_speakers(
audio_info['normalized'],
num_speakers=num_speakers
)
merged_segments = merge_transcription_with_diarization(all_segments, speaker_segments)
except Exception as e:
print(f"说话人分离失败({e}),跳过")
merged_segments = [{'start': s['start'], 'end': s['end'],
'speaker': 'SPEAKER_00', 'text': s['text']} for s in all_segments]
# 格式化转录文字
raw_transcript = ''
for seg in merged_segments:
time_str = f"[{int(seg['start']//60):02d}:{int(seg['start']%60):02d}]"
raw_transcript += f"{time_str} {seg['speaker']}: {seg['text']}\n"
print("Step 4: 修正转录错误...")
corrected_transcript = fix_transcription_errors(
raw_transcription=raw_transcript,
glossary=glossary,
context=meeting_info.get('title', '') if meeting_info else ''
)
print("Step 5: 生成会议纪要...")
minutes = generate_meeting_minutes(
corrected_transcription=corrected_transcript,
speaker_map=speaker_map,
meeting_info=meeting_info
)
# 保存结果
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
with open(f"{output_dir}/transcript_{timestamp}.txt", 'w', encoding='utf-8') as f:
f.write(corrected_transcript)
with open(f"{output_dir}/minutes_{timestamp}.md", 'w', encoding='utf-8') as f:
f.write(minutes)
print(f"完成!纪要已保存到 {output_dir}/minutes_{timestamp}.md")
return {
'transcript': corrected_transcript,
'minutes': minutes,
'duration': audio_info['duration_seconds'],
'output_dir': output_dir
}
# 使用示例
result = process_meeting_recording(
audio_path='/recordings/product_review_20240115.mp4',
output_dir='/output/meeting_20240115',
meeting_info={
'title': '产品需求评审会',
'date': '2024-01-15',
'attendees': ['张总', '李工', '王产品', '陈技术']
},
glossary=['用友', 'ERP', '钉钉', '金蝶', '飞书'],
speaker_map={
'SPEAKER_00': '张总',
'SPEAKER_01': '李工',
'SPEAKER_02': '王产品',
'SPEAKER_03': '陈技术'
},
num_speakers=4
)踩过的坑
坑一:说话人分离和录音质量强相关
多人同时说话、背景噪音大、麦克风距离远,说话人分离就会混乱。现实中会议录音质量普遍不太好。建议:对说话人分离的结果不要 100% 信任,生成纪要时标注"(说话人待确认)"让用户最终确认。
坑二:专有名词是最大的准确率杀手
每个公司都有自己的产品名、项目名、技术术语。花时间建一个公司专有名词词表,喂给 LLM 做修正,效果会好很多。这个词表要定期维护。
坑三:LLM 会"补全"它认为应该有的内容
有几次会议纪要里出现了根本没有讨论过的"决定"或"行动项"。这是 LLM 的幻觉问题,在 Prompt 里加强"只输出转录里出现的信息"有一定效果,但不能完全消除。建议对重要会议的纪要保留人工审核。
坑四:长会议的上下文丢失
三小时的会议,分段处理后,LLM 在生成纪要时看不到完整上下文,可能把前后相关的讨论割裂。解法是先生成每段的摘要,再对摘要做综合处理。
这个系统在公司用了半年,节省了大量整理会议纪要的时间。产品经理反馈说:90% 的普通会议不需要人工干预,直接用生成的纪要就行。另外 10% 是那种决策复杂、很多争论的会议,人工润色一遍就好了。
语音识别的准确率已经很高了,现在的瓶颈是纪要的质量,而纪要质量取决于会议录音质量和 Prompt 设计。这两个都是可以持续优化的地方。
