按标点切、拿句段切,两次都失败:救场的是词级时间戳里那 280 毫秒静音
最近在跟一条字幕轴较劲
手头有个小需求:给一段会议通知配字幕。文字部分不难,转写工具一大把,难的是时间轴,每条字幕什么时候出现、什么时候消失,得精确到能对上说话人的停顿。以前我的做法是导出全文,再在字幕软件里一句句拖,长录音拖下来眼睛都花了,而且拖完没有任何记录能说明这一刀为什么切在这儿。
这次想换个思路:如果转写接口本身就返回细粒度的时间戳,字幕轴是不是可以直接算出来,不用手拖?
阿里云百炼 CLI 的 bl speech recognize 能把语音转成文字,返回里带不带词级时间戳、细到什么程度,我得自己验一遍才知道。验法是用往返实验:先让 TTS 合成一段我知道原文的话,再让 ASR 把它转回来。原文在我手上,转写差在哪、时间戳细到哪一层,逐字对照就有答案,不用猜。
往返跑通,只有一个字不一样
合成一句会议通知:
bl speech synthesize --text "今天下午三点在第二会议室开选题会,记得带上周的数据报表。" --voice longcheng_v3 --out 通知.wav
再转回来:
bl speech recognize --url 通知.wav --language zh --out 通知.json
转写全文是这样:
今天下午3点在第二会议室开选题会,记得带上周的数据报表。
跟原文对照,我一个字一个字过了一遍,唯一差异是"三点"变成了"3点"。
我一开始以为这是识别错误,准备写个纠错规则。后来想明白了:这是 ASR 对数字做规范化输出,口语里的"三"落在时间语境里就写成阿拉伯数字。它不是错,是输出约定。字幕上要"三点"还是"3点",是我自己的文案口径问题,后处理统一替换就行,别把它当 bug 修。真去"修"它,反而可能改坏那些本来就该是阿拉伯数字的地方。
坑一:想按标点切,词级根本没有标点
时间结构比我预期的细。JSON 里分三层:全文、句级、词级。这句话的句级只有一段,160–4440ms;词级给了 16 个词,每个词都带起止毫秒。
看到 16 个词各带时间戳,我当时的想法是:这下省事了,按标点把词串切开,每段的首词起始毫秒当 cue 起点、末词结束毫秒当 cue 终点,字幕轴就出来了。
然后我把词面拼起来看了一眼:
今天下午3点在第二会议室开选题会记得带上周的数据报表
逗号不见了。词级输出不带标点,一个标点都没有。
这一刀切空了。句级那层是有标点的,可整句就一段,一段没法切成两条字幕。我坐在两条都不通的路中间:有时间的没标点,有标点的没时间粒度。
我还想过一个绕法:拿句级那层带标点的文本去跟词级序列做对齐,按位置把标点插回词流里,插回去就有标点可切了。理论上能写,代价是要多做一套字面对齐逻辑,比后面那个静音间隔的规则复杂得多。更要紧的是,就算对齐做成了,插回去的也是 ASR 自己的标点,不是原文标点。后面第二个坑会看到,它的标点确实会被改写。为了切个字幕条搭这么一套东西,我觉得不值,先放一放,回头看看有没有更省事的路。
那条路确实存在,就写在同一份 JSON 里,不过我得先栽第二个跟头才看见它。
坑二:想拿句段切,三句被并成了一段
那就退一步,用句段当字幕条吧,一句一条,标点也齐。
为了验证,我又合成了三句议题:"第一个议题是季度复盘。第二个议题,下月选题排期。散会前记得提交周报。"转写回来一看,三句被并成了一个句段,标点还被规范化了,"复盘。第二"变成了"复盘,第二"。
这条退路也不通,而且比坑一更隐蔽。如果我只跑了前面那句单句素材,根本发现不了:一句话本来就只有一个句段,"句段=字幕条"看起来完全成立。多句素材一上,句段数就不等于原句数了,条数直接错;句段里的标点也不等于原文标点,切点跟着错。
栽完这个跟头我回头想,验证素材本身也有讲究。第一段是单句、只有一个逗号,太"乖"了,很多结构问题在它身上不会暴露。第二段换成三句议题、句号和逗号都有,坑马上就出来了。以后验这类接口,样片得多备几种:单句、多句、带数字、停顿多的口语,各跑一遍再下结论。
两个坑的共性是同一件事:ASR 返回的文本层结构,是为转写结果服务的,不是为字幕切分服务的。指望它替我断句,指望不上。
救场的是那 280 毫秒
回头看词级时间戳那张表,答案一直摆在那里。
| 词 | 起(ms) | 止(ms) |
|---|---|---|
| 今天 | 160 | 400 |
| 下午 | 400 | 680 |
| 3 | 680 | 840 |
| …… | …… | …… |
| 会 | 2480 | 2640 |
| 记得 | 2920 | 3200 |
| 报表 | 4160 | 4440 |
"会"止于 2640ms,"记得"起于 2920ms,中间空了 280 毫秒。其余相邻词之间的间隔都是 0。
原文那个位置正好是逗号。说话人在逗号处换了一口气,音频里表现为一段静音,词级时间戳把这段静音如实记成了两个词之间的空隙。
所以断句不用问文本层,问时间层:相邻词的间隔超过阈值就断一条。我这轮阈值取 250ms,落在"逗号处 280ms"和"其余 0ms"之间,跑出来的 SRT 是:
1
00:00:00,160 --> 00:00:02,640
今天下午3点在第二会议室开选题会
2
00:00:02,920 --> 00:00:04,440
记得带上周的数据报表
两条 cue,断点正好落在原句逗号处。这份 SRT 是这轮跑出来的真实产物,两条的起止毫秒直接取自词级字段,我没有手工挪过任何一个时间点,cue 文本就是词面拼接的结果。
回头看这个过程,切字幕条这件事被我绕了一大圈:先想靠标点,标点不在词级;再想靠句段,句段会并段。真正管用的信号一直是最不像"文本"的那个,两个词之间的静音长度。
阈值这块得补一句:250ms 是这轮素材校出来的,不是通用常数。这轮的间隔分布很干净,除了逗号那 280ms 全是 0,取中间值很容易。换成语速快、停顿短的素材,250ms 可能一条都切不出来;换成停顿多的口语,又会被切得碎。我的办法是拿一段自己已知标点的素材先跑一遍,把相邻词间隔全列出来,看标点处和非标点处分别落在什么区间,再定阈值。
这轮学到的三件事
一是往返实验值得做。原文在手上,转写差异和时间戳粒度都能查实,比拿一段来历不明的录音试要踏实得多。
二是别把 ASR 的文本层结构当字幕结构用。词级没标点,句段会并段、标点会被规范化,两条路我都试过了,都不通。
三是时间戳里的"空隙"是有信息量的。280 毫秒的静音对应的就是那个逗号,比任何标点都可靠,因为它来自音频本身。
边界,写清楚
- 数字规范化:三→3 是 ASR 的输出约定,不是错误,字幕文案要哪个写法自己定
- 词级无标点:按标点切条不成立,静音间隔切分是本文方法,阈值需按素材自校
- 句段会并段:多句素材的句段数不等于原句数,别拿句段当字幕条
- 说话人分离未实测:
--diarization这轮没跑(素材是单说话人),多人会议里谁说了什么的效果不在本文结论内
还有一条:这轮的音频是 TTS 合成的,干净、单说话人、没有底噪也没有抢话。真实会议录音的条件差得多,切分表现会变,拿自己的素材重跑再定参数。
想试的话
三步就够:合成一句(或者直接拿你手上的一段录音)、bl speech recognize 转写、用词级时间戳的静音间隔切你的字幕轴。Key 在控制台密钥管理页领,音色和模型清单在阿里云百炼 CLI(bl)那一侧。
跑完建议先做一件小事:把转写全文和原文逐字对一遍,看数字写法差在哪。这种对照做多了,你就知道哪些后处理规则值得写进自己的字幕流水线,哪些根本不用管。

浙公网安备 33010602011771号