长录音送进去只返回开头几分钟,先别急着怀疑服务端。常见情况只有两类:一类是客户端在分片环节实际只发出了一片或前几片,另一类是分片都发了但请求体在长度上被截断。判断顺序建议固定下来:先量化返回文本覆盖了多长的时间范围,再回客户端日志数清实际发出去的片数,最后把每片的起止时间和对应返回片段排成一张表,看内容是从哪一片开始丢的。
先在返回结果里定位首尾时间戳,算出覆盖时长;再看客户端分片日志,确认发出去几片、每片字节数。若只发出一片,问题在分片或拼接逻辑;若多片都发出但只有前段有文本,往单次请求的输入长度方向核对。服务端的具体输入上限不臆断,以官方文档或自己做的对照记录为准。
先算清返回文本覆盖了多长的时间范围
「只有前半段」是感觉还是事实,取决于能不能把它落成两个数:原始录音总时长 T,以及返回文本覆盖到的最后一个时间点 E。如果返回结构里带 segment 或 word 级时间戳,直接取最大 end;如果只有纯文本,就在录音里回听返回的最后一句话,记下它出现的大致秒数,或者用这句里的关键词在音频时间轴上定位。覆盖时长的记录方式建议统一成三元组:总时长、覆盖结束点、以及返回的实际字数,后面每一步排查都对照这三个数,避免用「感觉砍了一半」来判断。
def coverage(resp, total_seconds):
segs = resp.get('segments') or []
if not segs:
return {'covered_end': None, 'note': '无时间戳,需回听定位'}
end = max(s.get('end', 0) for s in segs)
return {'covered_end': round(end, 1), 'total': total_seconds}
把这段输出贴进排查记录,后面每改一次分片长度都重跑一次,看 covered_end 是否变化。
在客户端日志里数清实际发出去几片音频
先把「客户端只发了一片」这条排除掉。分片日志至少要打印四个字段:片序号、这一片的起始秒、结束秒、字节数;再加一个布尔值标记这一片是否真的写进了请求体。核对方式有两条:所有片字节数之和与原始音频文件字节数比较,同一编码和采样率下应当接近;片数乘以每片秒数与总时长比较,差太多说明切片环节就漏了内容。
logger.info(
'slice index=%s start=%.1f end=%.1f bytes=%s sent=%s',
idx, start, end, len(chunk), sent,
)
如果日志里只有一条 slice 记录,或 sent 只有一片为真,那问题就在切片循环或发送分支,跟请求长度无关,直接去查切片函数和循环边界即可。
把每片的起止时间和对应返回片段排成一张表
把所有片和返回结果对齐,断点位置才会显出来。表格列建议固定为:片序号、起止秒、字节数、是否发出、该片返回覆盖的秒区间、返回字数。逐片建立对应关系时,如果请求是按片单独发的,直接把该片请求的返回值填进去;如果是一次请求带多片,就按返回时间戳落在哪一片区间来归属。
| 片序号 | 起止(s) | 字节数 | 是否发出 | 返回覆盖(s) | 返回字数 |
|---|---|---|---|---|---|
| 0 | 0-60 | 待填 | 是 | 0-60 | 待填 |
| 1 | 60-120 | 待填 | 是 | 60-120 | 待填 |
| 2 | 120-180 | 待填 | 是 | 空白或骤降 | 待填 |
断点通常表现为:某一片之前返回字数正常,从下一片开始返回为空或字数骤降。断点前后两片的差异要写清楚,比如「同为 60 秒切片,前一片返回字数正常,后一片返回为空」,这个差异是后面判断成因的直接依据。
调整分片长度后重跑同一段录音并对照差异
把分片长度做成参数,用同一段录音、同一套日志跑两次。参数化写法只需要改一个入口值,比如先 30 秒再 60 秒,其他流程不动,保证两次结果可比。
SLICE_SECONDS = 60 # 改成 30 再跑一次
slices = split_audio(path, slice_seconds=SLICE_SECONDS)
两次运行的对照维度固定为三个:返回覆盖时长、断点出现的位置、每片返回长度。读法上可以先按这几个方向看,再结合环境确认:断点在两次运行中都落在大致同一个时间点、且与片长无关,需要往单次请求的输入长度方向核对,具体上限以官方文档或自己的对照记录为准;断点从「第 N 片」变成「第 M 片」、跟着片序号走,更可能是分片文件或请求构造的问题;覆盖时长随片长变大而明显变长,则更可能是切片边界或拼接环节没接上。这些只是指向,不要在没做对照前就下死结论。
把确认可用的分片与拼接逻辑固化到脚本里
确认能跑通的组合定下来后,把切分、逐片请求、收集、按片序拼接、自检这五步写成固定骨架,下次遇到长音频直接跑,不再从日志里重新翻。重试的位置放在单片请求返回空或超时的分支内,只重试那一片,不重跑全量;重试仍为空时记成缺口,不要静默丢掉。
def run(path, slice_seconds=60, max_retry=2):
slices = split_audio(path, slice_seconds)
parts = []
for i, s in enumerate(slices):
text = ''
for attempt in range(max_retry + 1):
resp = call_asr(s.path)
text = (resp.get('text') or '').strip()
if text:
break
log.warning('empty slice i=%s start=%s attempt=%s', i, s.start, attempt)
parts.append({'i': i, 'start': s.start, 'end': s.end, 'chars': len(text), 'text': text})
self_check(parts)
return ''.join(p['text'] for p in parts)
def self_check(parts):
gaps = [p['i'] for p in parts if p['chars'] == 0]
covered = sum(p['end'] - p['start'] for p in parts if p['chars'])
log.info('slices=%s gaps=%s covered=%.1fs', len(parts), gaps, covered)
每次运行后看自检输出三件事:片数是否与预期一致,缺口片序号列表是否为空,覆盖秒数与录音总时长是否接近。任何一项对不上,就回到分片日志和上面的对照表重查,而不是直接改拼接字符串。