字幕转文字稿:去掉时间轴只是容易的那一半
所有字幕格式编码的都是同一件事:钉在时间轴上的短句语音。把它变回可读的文章,看上去像个五行脚本——删数字、删箭头、把剩下的接起来。拿这个脚本去跑一个真实的字幕文件,你会得到一份「差不多对」的东西,而这比明显错更麻烦,因为你得读一遍才发现。
有两件事会让它翻车。字幕的断行是为了塞进屏幕,不是为了句子结束,所以天真地拼接会得到每六七个字就被砍一刀的文本。而自动生成的字幕是滚动的,同一段话跨多条字幕重复出现,拼起来每句话会出现两三遍。这个工具两件事都处理。在上面上传或粘贴一个文件,结果立刻出现,就在这个浏览器标签页里。
滚动重复:真正的坑
这是最容易让人栽跟头的一条。YouTube 以及大多数直播系统的自动字幕并不输出一块块彼此独立的文本,它输出的是一个滑动窗口,好让观众看着字一个个累加上去。落到文件里就是这个样子:
- 字幕 1:千里之行
- 字幕 2:千里之行始于
- 字幕 3:始于足下
不加思考地拼起来,你得到的是「千里之行千里之行始于始于足下」——十四个字,而实际只说了八个。放到一场一小时的演讲上,长度大约翻三倍,这份文字稿也就没法读、没法搜、没法拿去总结了。
解决办法是只保留每条字幕新增的部分。对每条新字幕,工具会找出「既是已有文本的后缀、又是新字幕前缀」的最长一段,然后把它丢掉。用在上面的例子上,你会恰好一次地还原出「千里之行始于足下」。比较只针对最近若干字的有限窗口,而不是整份文字稿,这样在三小时的文件上依然很快,也不会把二十分钟后合理重复的一句话误删。
把句子重新接起来
一行字幕是按屏幕尺寸裁的:大约四十个字符、一次两行,断在文字正好碰到边缘的地方。一个句子横跨三四条字幕是常事。保持一条字幕一行,你得到的是一份技术上确实是文字稿、实际上没法读的文档。
段落模式会累积字幕,并在两个条件同时成立时断开:已经攒够了一段的量,且当前这条字幕以句末标点结束。这样断点永远不会落在半个意思上。同时有一个硬上限强制断开,因为有些人可以很久不说一个句号,而一整块不分段的文字本身也是另一种没法读。说话人一换就一定另起一段,这对访谈和圆桌很重要。
四种格式,同一个想法
格式是从文件内容本身判断的,不看扩展名,因为字幕文件被改名是家常便饭,一个装着 SubRip 数据的 .txt 很常见。
- SubRip(.srt)——一个序号、一段用逗号作小数点的时间轴,然后是文本。产出最多、也最简单的格式。
- WebVTT(.vtt)——浏览器标准。小数点用点不用逗号,另外允许
NOTE、STYLE和REGION块,这些不是台词,解析前就被移除。VTT 还允许行内标记:<v Speaker>标明是谁在说,<c.classname>施加样式,字幕内部的裸时间戳驱动卡拉 OK 式的逐词高亮。这些全部会被解包,其中 voice 标记会变成说话人标签。 - YouTube(.sbv)——单独一行、用逗号分隔的一对时间码,然后是文本。没有序号,没有标记。
- Advanced SubStation(.ass 和 .ssa)——一份声明了列顺序的事件表。解析器读那行
Format:,而不是假定字段位置,因为 Text 字段被放在最后,正是为了让它可以包含逗号。花括号里的覆盖块是样式指令,会被移除;\N换行变成空格。Name 列有内容时就是说话人。
说话人、音效,以及该留下什么
说话人信息按格式不同有三种来路:ASS 里的专用字段、VTT 里的 voice 标记,或者 SRT 里那个纯靠约定的写法——行首写 姓名:,因为 SRT 根本没有说话人字段。三种都会被识别并归一成同样的输出。第三种的启发式规则刻意收得很窄——必须是行首一小段像名字的短词再跟一个冒号——否则任何含冒号的普通句子都会被误判,把整份文字稿撕碎。
[MUSIC]、(laughter)、[APPLAUSE] 这类方括号音效提示默认保留,可以用开关去掉。哪种对,完全取决于你要干什么:做可检索的档案或无障碍记录,它们就是内容的一部分;把一场演讲喂给模型做总结,它们就是噪声。
三种输出形态
预设覆盖了人们真正想要的东西。干净文字稿是不带时间码的连贯段落,适合阅读、引用,或交给模型。带时间戳保留段落,但给每段标上起始时间,当文字稿的作用是帮你跳回视频里的某个瞬间时,你要的就是它。字幕行保持一条字幕一行、带时间码、不去重——最接近原始文件的形态,适合你在检查字幕本身而不是通读内容的时候。
输出旁边,工具会报告识别到的格式、字幕条数、字数、覆盖时长,以及删掉了多少重复。最后那个数字是最诚实的一个:它大,就说明源文件是自动生成的滚动字幕,你也能确切看到原始文件里有多少是重复。
什么都不会离开浏览器
字幕文件并不总是公开的。访谈录音、内部全员会、法庭庭审、问诊记录、未发布的素材,都会产生字幕。解析由这个标签页里的 JavaScript 完成,文件在本地读取,从不上传。这也是为什么转换是瞬时的,以及为什么改一个选项会立刻重新渲染,而不是再跑一趟往返。