标注 ASR 转写稿:一个完整实例
从一个 Whisper 输出目录一路走到带标签的说话人话轮:怎么选标注单元、怎么处理说话人分离、怎么写配置、怎么把任务跑起来,以及怎么在导出时保住时间对齐。
这篇文章把一个项目从头走到尾:40 段录下来的研究访谈,已经用 Whisper 转写过,需要按主题编码,还要标出谁说了什么。它是那两篇参考指南的具体版本,每个决定都真的做了,而不是描述了一遍。
如果你要的是逐格式的细节,那部分在转写格式里。
检查你保留了什么、决定谁来标说话人、写好配置、带着时间戳导出
第 0 步:先看看你手里到底有什么
别的都先放一边,先弄清楚这个目录里是什么。这一步花十秒钟,能省你一天:
potato transcripts ./whisper_out --dry-runScanned 40 file(s):
interview_01.json Whisper JSON 42 turns 891.4s undiarized
interview_02.json Whisper JSON 51 turns 1120.8s undiarized
interview_03.txt plain text 1 turns 0.0s undiarized
...
40 item(s), 1683 turn(s).
从里面能读出两件事。所有文件都没做说话人分离,所以这份语料里没有任何东西知道说话的是谁。另外 interview_03.txt 被当成了一个时长为零的话轮,因为 Whisper 出来的 .txt 里只有文字,别的什么都没有。里面根本没有时间信息可以找回来。
第三个文件要么得把对应的 .json 找出来,要么把音频重跑一遍。下游没有任何环节能补救它。
第 1 步:先定标注单元,再定标签
单元这个问题对你的一致性数字的影响,比标签集大得多。
Whisper 的分段大致是话语级的,它在停顿处断开,而不是在任何语法边界上断开。对访谈编码来说这通常就是对的单元:受访者的一次回答会分成好几段进来,把每段单独编码,得到的记录比把整段回答一起编码要细。
会出问题的是视频平台的自动字幕,那里的字幕行边界落在字幕框填满的地方。让标注者在这样的字幕行上给“每个句子”打分,产生的分歧是关于句子边界在哪,而不是关于你想测的那个东西。如果这就是你的输入,请看如何标注 YouTube 字幕。
这里的分段可以直接用,所以单元就是话轮。
第 2 步:决定由谁来分配说话人
Whisper 不做说话人分离。有三个选项,而且这是一个真正的决定,不是走过场:
用 WhisperX 重跑一遍。 自动、快,但在重叠说话上错得够频繁,反正总得有人来核一遍。
whisperx interview_01.mp3 --model medium --diarize --output_format json用一个开了说话人分离的云端 API。 Deepgram 加 diarize=true、AssemblyAI 加 speaker_labels、AWS Transcribe,或者 Rev.ai。这四种 Potato 都能原生读。
让标注者一边听一边分配说话人。 对 40 段双人访谈来说,我们会选这个。两个角色分明的说话人对人来说是最容易的情形,对模型却不一定,而且标注者本来就在听音频。
我们选第三个。没做说话人分离的话轮会显示为 Unassigned 并带一个选择器,分配结果和标注一起保存。
未做说话人分离的话轮以 Unassigned 到达,每条上都有一个选择器
第 3 步:造数据文件
potato transcripts ./whisper_out --media-dir ./audio -o data/interviews.json转写稿按文件名主干和音频配对,所以 interview_01.json 会找到 interview_01.mp3。Whisper 那种重复的 interview_01.mp3.json 命名也处理了,条目 id 出来就是 interview_01。
结果是:
{
"id": "interview_01",
"conversation": {
"audio": "audio/interview_01.mp3",
"turns": [
{"turn_id": "t0", "speaker": null, "start": 0.0, "end": 6.5,
"text": "So I want to start with how the team was structured."}
]
}
}如果你更愿意把转写稿保持成文件,这一步完全可以跳过。数据文件可以直接指向它们,Potato 会在渲染时读取并归一化:
{"id": "interview_01", "conversation": {"audio": "audio/interview_01.mp3",
"transcript": "whisper_out/interview_01.json"}}第 4 步:写配置
annotation_task_name: "Interview Coding"
task_dir: .
data_files:
- data/interviews.json
item_properties:
id_key: id
text_key: conversation
instance_display:
fields:
- key: conversation
type: audio_dialogue
label: "Interview"
span_target: true
display_options:
show_timestamps: true
scroll_height: 520px
allow_speaker_assignment: auto
speakers:
- id: interviewer
name: "Interviewer"
color: "#7c3aed"
side: left
- id: participant
name: "Participant"
color: "#059669"
side: right
annotation_schemes:
- annotation_type: radio
name: turn_topic
description: "What is this turn about?"
labels: [structure, workload, tooling, morale, other]
- annotation_type: span
name: quotes
description: "Highlight anything quotable in the writeup"
labels:
- name: quotable
key_value: "q"这里有三处在干活。
说话人名册在还没有人分配过任何一个话轮之前,就给这两个角色定好了稳定的名字、颜色和左右位置。没有它,说话人也还是会拿到颜色,但那是每份转写稿各自确定性地分配的,不是全语料一致的。
turn_level: true 配上 turn_binding,把主题问题挂到每一个话轮上,而不是挂到整场访谈上。这才是让话轮在实际操作中成为标注单元的东西。
span_target: true 加上 span 方案,让一段高亮可以跨越话轮边界,当值得引用的段落横跨一个问题和它的回答时,这一点很重要。说话人被重新分配时,偏移量保持稳定。
跑起来:
python potato/flask_server.py start config.yaml -p 8000
每个话轮都有一个只播放这一轮的播放按钮,以及它自己的标注问题
每个气泡上都有一个播放按钮,只播这一个话轮然后停下。实际用起来,这是标注者会主动提起的功能:拿某一句去核对音频,不再是一场拖进度条的苦差事。
第 5 步:在对的东西上看一致性
每场访谈两位标注者,于是你有两种一致性要看。
主题标签是话轮层面上普通的分类一致性。因为话轮 id 是确定性的,同一个文件永远产生同样的 id,两位标注者的标签不需要任何对齐步骤就能对上。
说话人分配值得单独看。如果两位标注者在 15% 的话轮上对“谁在说话”有分歧,那是关于音频本身的一个信号,也意味着自动说话人分离至少会以同样的频率出错,而且不会告诉你。
一致性指标见标注者间一致性,高亮部分见跨度与结构化输出的一致性,后者需要不同的处理方式,因为标注者除了选标签还要选边界。
第 6 步:导出
标准的 JSON、JSONL 和 CSV 照常可用。如果你想让时间对齐一路活到语音分析工具里,就把分层标注导出成 ELAN 或 Praat 格式:
python -m potato.export --config config.yaml --format eaf --output ./out/
python -m potato.export --config config.yaml --format textgrid --output ./out/两者都能双向往返,因为 Potato 也能把 EAF 和 TextGrid 当输入读。在这里标注,在 ELAN 里精修,再把结果读回来。
会出问题的四件事
有人留的是 .txt。 没有时间信息,找不回来,只能重跑。--dry-run 会在你在它上面搭起任何东西之前就抓住它。
时间差了 1000 倍。 上游某个环节把秒和毫秒混了。Whisper 和 Deepgram 输出的是浮点秒;AssemblyAI、whisper.cpp 的偏移量,以及 Whisper 的 TSV 输出的是整数毫秒。
在字幕行输入上套句子级方案。 上面讲过了,也是这四件里代价最高的一件,因为你要等到算一致性的时候才会发现。
信任了没人核过的说话人分离。 一处分离错误会传染到挂在那个话轮上的每一个标签,而当你回头找原因时,它看起来像是标注者分歧。
延伸阅读
- 如何标注 Whisper 转写文本
- 如何标注 YouTube 字幕
- 转写格式
- Transcript Format Ingestion,一个把六种格式并排展示的可运行示例设计
- Potato 2.7.1:转写稿本来就有了