长音频转写的工程实践:分片、轮询与断点续传
长音频转写的工程实践:分片、轮询与断点续传
把一段两小时的会议录音丢给语音转文字(STT)接口,新手工程师通常在三个地方翻车:请求超时、单次转写内存爆掉、失败重试只能整段重来。这篇把一套经过实战的分片转写流水线拆开讲。
一、为什么必须分片
长音频直传有三个结构性问题:
- 超时上限:同步接口普遍 60 秒墙,两小时音频根本进不去;异步接口虽无此墙,但单任务越大,失败重跑的代价越高
- 内存峰值:整段载入+解码,8 小时播客能吃掉数 GB 内存
- 重试粒度:95% 成功 + 5% 失败 = 100% 重来,这是最亏的
分片把失败半径从"整段"缩到"单片",三个问题一起解。
二、怎么切:固定窗口 vs 静音检测
两种切法:
| 切法 | 实现 | 优点 | 缺点 |
|---|---|---|---|
| 固定窗口 | 每 60~120 秒一刀 | 实现零成本 | 词被拦腰切断,识别质量受损 |
| 静音检测 | 按 VAD 找静音点下刀 | 词界完整,质量最好 | 依赖 VAD 参数,安静录音会切不开 |
工程折中:固定窗口 + 窗口间 1~2 秒重叠。重叠区转写后按时间戳去重,实现简单,词被切断的概率大幅下降。除非业务对断句质量极敏感,不必上 VAD。
CHUNK_SEC, OVERLAP_SEC = 90, 2
def slice_audio(duration):
starts = list(range(0, duration, CHUNK_SEC - OVERLAP_SEC))
return [(s, min(s + CHUNK_SEC, duration)) for s in starts if s < duration]
三、轮询:指数退避 + 状态机
异步转写提交后进入轮询。两个要点:
def poll_task(task_id, client):
wait = 2.0
while True:
state = client.task_status(task_id) # PENDING/RUNNING/DONE/FAILED
if state in ("DONE", "FAILED"):
return state
time.sleep(wait)
wait = min(wait * 1.6, 30) # 指数退避,封顶 30s
- 指数退避:刚开始 2 秒一查,逐步拉长到 30 秒封顶——短任务快速收口,长任务不空转
- 总超时必须有:单片转写超过 10 分钟视为挂死,走重试逻辑,防止死循环
四、断点续传:分片级 checkpoint
流水线任意时刻可能被打断(进程重启、机器发布)。把每片状态落盘,重启后跳过已完成的片:
def transcribe_all(chunks, store):
results = {}
for i, (start, end) in enumerate(chunks):
if store.done(i): # checkpoint 命中,跳过
results[i] = store.load(i)
continue
task = client.submit(chunk_file(i))
if poll_task(task.id, client) == "DONE":
store.save(i, client.fetch(task.id))
results[i] = store.load(i)
else:
raise RetryableChunk(i) # 只重试这一片
return merge(results) # 按时间戳合并 + 重叠区去重
checkpoint 用一个 JSON 文件就够,别过度设计。核心收益:任何一片失败,代价只是那一片。
五、选接口时看三个参数
- 单次时长上限:决定分片窗口的硬顶
- 异步任务支持:有没有任务号+查询接口,决定轮询模式还是同步等待
- 并发配额:分片转写是并发友好型负载,QPS 配额直接决定总耗时
按量计费的 STT 平台(如算邻坊 suanlinfang.cn 的随心语音转文字)这类按秒计费、支持异步任务的接口,配这套流水线刚好——分片天然对账,转多少秒付多少秒,失败片重试不产生重复计费。
六、小结
长音频转写的工程化 = 分片缩失败半径 + 退避轮询收状态 + checkpoint 保断点。三者都是几十行代码的事,但没有它们,转写服务在生产环境就是定时炸弹。上一篇《按量计费的工程账》聊了钱怎么算,这篇聊活怎么干,两篇合起来是一套完整的按量服务使用姿势。
浙公网安备 33010602011771号