上一篇的 TTS 把文字变成语音,本篇把方向反过来:语音怎么变回文字。ASR(Automatic Speech Recognition,自动语音识别)是 RAG 与 Agent 接收语音输入的基础设施——播客转文字入库、会议录音变纪要、语音指令进 Agent,第一站都是它。本篇先分清离线转写与流式识别两种形态,再过 OpenAI API 与本地 Whisper 两套方案的用法,落到中文质量、时间戳、说话人分离三个工程细节,最后串成一条会议录音的结构化流水线。全文按"形态 → 转写 API → 本地部署 → 中文质量 → 时间戳 → 说话人 → 实战"组织。
1、离线转写与流式识别
ASR 的第一道分叉不是选模型,而是选形态。离线转写吃整个音频文件、一次性吐全文,延迟按"音频时长 ÷ 转写速度"计,几十分钟录音等几十秒出稿完全可用;流式识别吃麦克风实时分片,边说边出,首字延迟以百毫秒计。两者背后是不同的接口形态与计费逻辑。
| 维度 | 离线转写 | 流式识别 |
|---|---|---|
| 输入 | 完整音频文件 | 麦克风实时分片 |
| 延迟 | 秒级,事后出稿 | 首字数百毫秒级 |
| 场景 | 纪要、字幕、播客入库 | 语音输入、实时字幕、语音 Agent |
| 计费 | 按音频分钟数 | 按会话时长 |
两条链路的形态对比如下:
差别集中在第一跳:离线等完整文件、攒完再算,流式从第一片就开工——后面的延迟与计费差异都是这一跳的延伸。
流式识别的现状:OpenAI 的 Realtime API 提供转写会话(transcription session),WebSocket 连 wss://api.openai.com/v1/realtime?intent=transcription,推音频分片进去、增量文本事件出来,模型仍是 gpt-4o-transcribe 一系;WebRTC 通道留给"边听边聊"的完整语音对话,纯转写走 WebSocket。官方新的实时转写模型 2026 年仍在演进,生产选型以官方音频指南当日文档为准。离线转写才是本篇主战场——纪要、字幕、RAG 入库三大场景全是它。
两条形态对应两类延迟预算:语音 Agent 的首字延迟要压在 500 ms 内,慢半拍用户就察觉停顿;RAG 入库的播客转写是后台任务,分钟级无人感知。形态先选对,再谈模型——形态错了,模型再好也救不回延迟。
2、OpenAI 转写 API
转写走 client.audio.transcriptions.create,一个文件、一个模型、几个参数。坑在参数支持度:gpt-4o-transcribe 精度更好但参数被裁剪——verbose_json、字级时间戳、prompt 词汇引导都只有 whisper-1 支持,文件还有 25 MB 上限。参数最全的调用长这样:
# openai_transcribe.py —— whisper-1 转写(参数支持最全的模型)
from openai import OpenAI
client = OpenAI()
with open("meeting.m4a", "rb") as f:
result = client.audio.transcriptions.create(
model="whisper-1", # 字级时间戳/prompt 仅此模型支持
file=f,
language="zh", # 显式指定,防语种误判
prompt="术语表:K8s、RAG、MCP、Agent", # 词汇引导,纠专有名词
response_format="verbose_json", # 才有 words/segments
timestamp_granularities=["word", "segment"],
)
print(result.text) # 全文
print(result.words[0].start) # 词级起点,秒
print(result.segments[0].start) # 句级起点,秒
三个参数值得记:language="zh" 防止开场一段英文把语种带偏;prompt 不是指令而是词汇表,专有名词纠偏全靠它;要时间戳必须配 verbose_json。当前价格(2026 年 10 月,见官方价格页,每分钟 $0.006 约合每小时 $0.36):
| 模型 | 价格 | 字级时间戳 | prompt | 说话人 |
|---|---|---|---|---|
| gpt-4o-transcribe | $0.006/分钟 | 无 | 无 | 无 |
| gpt-4o-mini-transcribe | $0.003/分钟 | 无 | 无 | 无 |
| whisper-1 | $0.006/分钟 | 有 | 有 | 无 |
| gpt-4o-transcribe-diarize | 按 token | 无 | 无 | 有 |
选型一句话:只要精度选 gpt-4o-transcribe;要时间戳或词汇引导,回 whisper-1,或者干脆本地——下一节的 faster-whisper 两者都免费给你。
25 MB 上限意味着长录音先过 ffmpeg:ffmpeg -i in.m4a -ar 16000 -ac 1 out.mp3,单声道 16 kHz 既缩体积又贴近训练数据,一小时音频稳稳压进限制内。
批量跑量还有个省钱组合:先用 mini 档全量转一遍定位内容,再对重点段落换大模型精转,账单立减一半以上,精度损失只出现在无所谓的段落上。
3、本地转写方案
本地部署系列第 3 篇讲过 llama.cpp 的思路:量化权重、CPU 也能跑。Whisper 生态有两位同门——faster-whisper 用 CTranslate2 引擎,whisper.cpp 用 ggml 权重,与 llama.cpp 一脉相承。要便利付钱走 API,要批量与隐私就本地,成本为零。
3.1、faster-whisper
faster-whisper 把官方 Whisper 权重转成 CTranslate2 格式,同精度下推理快约四倍、内存更省,是 Python 侧批量转写的默认选择。批量模式进一步吃满 GPU,用法与单文件转写同构:
# local_transcribe.py —— faster-whisper 批量转写
from faster_whisper import WhisperModel, BatchedInferencePipeline
model = WhisperModel("large-v3", device="auto", compute_type="auto")
pipeline = BatchedInferencePipeline(model=model) # 批量推理,吞吐数倍提升
segments, info = pipeline.transcribe(
"meeting.wav",
language="zh",
batch_size=16, # 0 则退回逐段推理
word_timestamps=True, # 字级时间戳
vad_filter=True, # 静音过滤,防幻觉串场
initial_prompt="术语表:K8s、RAG、MCP、Agent", # 词汇引导
)
for seg in segments:
print(f"[{seg.start:6.2f} -> {seg.end:6.2f}] {seg.text}")
对照第 2 节:同样的 large-v3、同样的字级时间戳与词汇引导,一行账单不产生。显存吃紧就 compute_type="int8",8 GB 级显卡乃至纯 CPU 都能跑,只是吞吐下来。
3.2、whisper.cpp
纯 CPU 服务器、树莓派、离线内网,选 whisper.cpp——C++ 实现、ggml 量化权重、零 Python 依赖。部署一条龙:
# 下载、编译、跑转写
git clone https://github.com/ggml-org/whisper.cpp && cd whisper.cpp
cmake -B build && cmake --build build -j --config Release
bash ./models/download-ggml-model.sh large-v3
./build/bin/whisper-cli -m models/ggml-large-v3.bin -f meeting.wav -l zh -osrt
-osrt 直接产出 SRT 字幕文件,-l zh 锁中文;内存紧张就把 large-v3 换成量化版权重。分工一句话:GPU 服务器用 faster-whisper,CPU 与端侧用 whisper.cpp,两者精度同源,差的只是工程形态。
4、中文识别质量
定性的表现口径(large-v3 档位):普通话日常口语相当能打,标点断句直接可用;专业词与中英混读是重灾区——"预训练"写成"与训练"、"召回率"写成"招回率"、"Kubernetes"被音译成中文,都是高频错例;带口音的普通话准确率下滑,粤语、川渝等方言明显不可靠。中文特化模型(阿里 FunASR、SenseVoice 一系)在普通话与中英混读上口碑更好,重中文的业务值得对比测试后再定。
工程对策按收益排序。第一是词汇引导:initial_prompt 塞进术语表,专有名词错误率立降——这也是本地的隐性优势,API 侧只有 whisper-1 支持 prompt。第二是 VAD 过滤:vad_filter=True 剔掉静音段,长录音里的"幻觉复读"(模型在静音处编出重复句)大半消失。第三是 LLM 后纠:转写稿连同术语表发给 chat 模型改错,同音字、专有名词、标点一把修掉,本系列讲过的 LLM 后处理套路直接复用。
5、时间戳粒度与用途
时间戳让转写从"一段文字"变成"结构化数据"。句级(segment)给每句话起止秒数,字幕与会议时间轴够用;字级(word)精确到每个字,逐字高亮跟读、音文对齐、RAG 命中后回放原音频片段,都要它。参数对照:whisper-1 用 timestamp_granularities,faster-whisper 用 word_timestamps=True,第 2、3 节代码里都带了。以播客场景为例:字级时间戳把每句话锚回原音频,检索命中后可以直接跳到那句原声播放,逐字稿跟着读音滚动高亮——这是纯文本转写给不了的体验。
后处理举一例:字级时间戳按标点聚合成 SRT 行:
# to_srt.py —— 字级时间戳按标点聚合输出 SRT
def fmt(t):
h, rem = divmod(t, 3600)
m, s = divmod(rem, 60)
return f"{int(h):02d}:{int(m):02d}:{int(s):02d},{int(s % 1 * 1000):03d}"
def to_srt(words):
srt, buf, start, n = [], [], None, 0
for w in words:
if not buf:
start = w["start"]
buf.append(w["word"])
if w["word"][-1] in "。?!,;":
n += 1
srt.append(f"{n}\n{fmt(start)} --> {fmt(w['end'])}\n{''.join(buf)}\n")
buf = []
return "\n".join(srt)
聚合逻辑就一句话:攒词到标点断行,行的时间戳取首词起点与尾词终点。注意字级时间戳在超长音频上会累积漂移,30 分钟以上的录音建议先按静音切段再逐段转写,时间轴更稳。
6、说话人分离
Diarization(说话人分离)回答"谁在说",与"说了什么"是两个任务:转写模型听内容,分离模型听音色。方案两路。云上一条:gpt-4o-transcribe-diarize 在文件转写里直接输出说话人标签,按 token 计费,省事但仅限文件接口,Realtime 不支持。本地一条:pyannote.audio 是事实标准,输出"时间段 → 说话人"列表:
# diarize.py —— pyannote 说话人分离
from pyannote.audio import Pipeline
pipeline = Pipeline.from_pretrained(
"pyannote/speaker-diarization-3.1",
use_auth_token="hf_xxx", # 需 HuggingFace 账号并接受模型条款
)
result = pipeline("meeting.wav", num_speakers=2) # 已知人数时显式指定更稳
turns = [(t.start, t.end, spk)
for t, _, spk in result.itertracks(yield_label=True)]
分离结果只有时间没有文字,输出的是 SPEAKER_00 这类匿名标签,人数已知时传 num_speakers 能明显减少串人。要变成可读纪要,得跟字级时间戳合并——下一节就干这个。
7、会议记录流水线实战
把前六节串成一条流水线:会议录音 → 字级转写与说话人分离并行 → 时间轴合并 → LLM 纪要。转写与分离都吃原始音频,天然可并行;合并策略取重叠最大归属——每个词找与其时间重叠最大的说话人段:
# meeting.py —— 转写 × 分离合并成结构化记录
def flatten(segments):
return [{"word": w.word, "start": w.start, "end": w.end}
for s in segments for w in s.words] # 词流摊平
def speaker_of(word, turns):
# turns: [(start, end, speaker)],与词重叠最大的段胜出
return max(turns, key=lambda t:
min(word["end"], t[1]) - max(word["start"], t[0]))[2]
def minutes(words, turns):
out, cur = [], None
for w in sorted(words, key=lambda x: x["start"]):
if (spk := speaker_of(w, turns)) != cur: # 换人另起一行
out.append(f"\n[{w['start']:6.1f}s] {spk}: ")
cur = spk
out[-1] += w["word"] # 中文直接拼接
return "".join(out)
flatten 把第 3 节的 segments 摊平成词流,minutes 按"换人就换行、行首打时间戳"输出逐行记录,SPEAKER_00 可再映射成真名。开工前先统一音频格式:ffmpeg -i in.m4a -ar 16000 -ac 1 meeting.wav,两个模型吃同一份 16 kHz 单声道,省去各自解码的坑。
这份记录人读可直接归档;再往上一层,连同术语表发给 LLM,产出议题、结论、待办三段式纪要。纪要提示词给两样东西就够:全文术语表、三段式输出骨架,质量立刻稳定。一小时会议录音,large-v3 在单卡上几分钟到十几分钟出全套,边际成本为零——这是本地流水线对 API 的最大优势。
8、总结
回顾全文:形态上离线转写管"事后出稿"、流式识别管"边说边出",Realtime 转写会话走 WebSocket;API 上 gpt-4o-transcribe 精度好但参数被裁剪,字级时间戳与词汇引导要回 whisper-1,两者都是每分钟 $0.006;本地上 GPU 选 faster-whisper、CPU 选 whisper.cpp,large-v3 免费自持;中文质量靠术语表引导、VAD 过滤、LLM 后纠三板斧;说话人分离交给 pyannote,按重叠最大与字级时间戳合并,落地成结构化会议记录。
留一个练习:找一段 20 分钟的访谈录音,跑通本地流水线——faster-whisper 出字级转写、pyannote 出说话人段、合并成带时间戳的访谈稿,最后让 LLM 把口语转成书面访谈录。做完对比加与不加 initial_prompt 时专有名词的错误率差异,本篇的知识点就都过手了一遍。
对于手里有大量录音资产(会议、播客、客服)的团队,建议先把本地 faster-whisper 流水线跑通再谈 API——数据不出门、成本归零,中文术语引导也比 API 侧更可控。

