Nemotron 3.5 Transcribe 这类转写模型通常只对「文本」负责,时间戳多半落在分段级,甚至只到词或字一级,句子边界要靠后处理补。判断顺序建议是:先打印一条完整输出确认字段与时间单位,再决定时间戳在并句环节取首尾,还是交给强制对齐工具重算;字幕不对齐时不要先怀疑模型,先看自己的合并规则。
把转写结果当成「文本 + 分段边界」,不要指望它直接产出句子级字幕。先用日志打印一条完整输出,确认 start/end 是秒还是毫秒、层级在哪一层;再用标点与停顿阈值并句,时间戳取首段起点、末段终点;需要严格句界时再接强制对齐。验收靠随机抽十句回听,把整体偏移和逐句漂移分开判断,不要只看第一句。
先打印一条完整输出,看清字段结构
不要凭记忆写解析代码。把一次请求的原始返回落盘成文件,再用 python -m json.tool 或 jq 展开看一遍,确认三件事:文本在哪个字段、时间戳挂在哪一层、分段是按什么切的。下面只是一个常见形态的样例,字段名和层级要以你本地实际运行结果为准,不要照抄。
{
"text": "整段转写的完整文本",
"segments": [
{
"start": 0.0,
"end": 2.36,
"text": "我们把这一段先切出来",
"tokens": [
{"text": "我们", "start": 0.0, "end": 0.42}
]
}
]
}text:整段纯文本,不含时间信息,只能用来做全文比对或搜索,不能直接切字幕。segments:分段数组。粒度由模型按停顿切,可能一句一段,也可能半句一段,这是碎段的来源。start/end:该段在音频中的起止。重点确认单位:秒的浮点数还是毫秒整数。若数值都是整百整千量级,基本可以判断是毫秒。tokens:字或词级时间戳。有这一层就有条件做近似句子级对齐;没有这一层,句子级时间戳只能靠分段边界推算。
验证方式:取第一条 start 与最后一条 end,跟音频实际时长比对。起点明显大于 0,说明开头静音被吃掉;终点明显小于音频长度,说明尾部有内容没被识别出来。这两种情况都会让后续字幕整体错位,先修输入再谈对齐。
把碎段合并成可读句子
碎段的典型表现是:字幕一行只有三五个字,闪一下就走,读起来跟不上。合并逻辑建议按顺序累积相邻分段,满足任一条件就断句:累积文本以句末标点结尾、相邻两段的间隔超过停顿阈值、累积字符数达到上限。停顿阈值从 0.5 秒到 0.8 秒起调,按素材语速调整,不要写死在业务代码里。
END_PUNCT = '。!?!?;;'
def flush(buf):
return {
'start': buf[0]['start'], # 取第一段起点
'end': buf[-1]['end'], # 取最后一段终点
'text': ''.join(s['text'] for s in buf).strip(),
}
def merge(segments, pause=0.6, max_len=32):
out, buf = [], []
for seg in segments:
if buf and seg['start'] - buf[-1]['end'] >= pause:
out.append(flush(buf)); buf = []
buf.append(seg)
text = ''.join(s['text'] for s in buf).strip()
if (text and text[-1] in END_PUNCT) or len(text) >= max_len:
out.append(flush(buf)); buf = []
if buf:
out.append(flush(buf))
return out合并后时间戳的规则要写死并注释清楚:起始取被合并的第一段 start,结束取最后一段 end,不在字之间做插值,也不取平均。风险边界在于:如果两句之间夹着一段长静音,按首尾取时就会让字幕长时间停留,观感很差。稳妥做法是先按停顿断句,再做二次合并,或者把超过阈值的长静音从句子内部切出去。
句子级时间戳的两种补法
近似路线:用分段边界推算。句子由若干分段拼成,直接沿用首尾时间戳。适用条件是模型分段粒度已经接近句子、语速比较均匀、用途只是普通观看字幕。代价是句内起止并不严格对应第一个字和最后一个字,遇到快语速或句首轻声时偏差会更明显。这条路线不需要额外算力,改配置和脚本就能跑,建议先做。
精确路线:用强制对齐工具补。把原始音频和已经确认过的转写文本一起送进对齐器,拿到字或词级边界,再按分词结果聚合成句子。适用条件是必须有原始音频、文本与音频严格一致、对句界要求高(例如逐句高亮或教学场景)。代价和对齐风险要说清:文本被改写、漏字、多字都会让对齐失败或错位;中文还要处理分词边界,标点不能参与对齐;另外对齐步骤会明显增加处理耗时,需要在流水线里单独排队。判断顺序建议是先近似,抽检不过再上精确。
导出 SRT 时容易踩的格式坑
SRT 的失败大多不在内容,而在格式:时间用点号而不是逗号、毫秒没补三位、序号不连续、块与块之间少一个空行,播放器就直接整段不认。下面是最小可用的生成骨架,fmt 负责补零与逗号,写文件时统一 UTF-8 编码,并保证结束时间严格大于开始时间。
def fmt(t):
ms = int(round(t * 1000))
h, ms = divmod(ms, 3600000)
m, ms = divmod(ms, 60000)
s, ms = divmod(ms, 1000)
return f'{h:02d}:{m:02d}:{s:02d},{ms:03d}'
def to_srt(items):
lines = []
for i, it in enumerate(items, 1):
start = it['start']
end = max(it['end'], start + 0.001) # 防止零长度
lines.append(str(i))
lines.append(f'{fmt(start)} --> {fmt(end)}')
lines.append(it['text'].strip())
lines.append('')
return '\n'.join(lines)一条输出样例,可以直接对照检查:
1
00:00:00,000 --> 00:00:02,360
我们把这一段先切出来
2
00:00:02,400 --> 00:00:05,120
再决定时间戳补在哪一环另外两个细节:句子内部的换行要保留,但不要出现连续两个空行;文件末尾留一个换行即可,多余的空白块会让部分播放器把最后一条当成非法条目。导出后用播放器打开一次,能正常显示再进入抽检环节。
随机抽十句回听核对对齐
抽检的目的不是证明「全部正确」,而是判断偏差是整体偏移还是逐句漂移。抽查清单建议这样定:随机取十句,覆盖开头、中段、结尾各至少两句;其中包含一句带数字或专有名词的、一句语速明显偏快的、一句句首有停顿的。回听时人工判断语音实际起止,与字幕起止相减,正值表示字幕晚于语音。
| 句子(首几字) | 起始偏差 | 结束偏差 | 备注 |
|---|---|---|---|
| 待填 | 待记录 | 待记录 | 开头段 |
| 待填 | 待记录 | 待记录 | 含数字 |
| 待填 | 待记录 | 待记录 | 语速快 |
读表方法:十句偏差方向一致、幅度接近,基本是整体偏移,优先查音频起点、采样偏移、前置处理有没有裁静音;偏差方向随机、幅度忽大忽小,说明分段边界本身不准,回到并句阈值调整,或者改用强制对齐;如果起始偏差小而结束偏差大,句子被拉长了,多半是尾部静音被并进同一句。把这份记录留档,下次换素材时可以直接对比,比凭感觉说「好像对上了」可靠。