说话人分离标注
在 Potato 中构建说话人识别任务:交互式音频波形、时间戳标记、说话人标签分配,以及标注者间一致性测量。
本教程在 Potato 中构建两个说话人标注任务。第一个任务中,标注者在波形上从零开始标出说话人轮次。第二个任务中,他们修正上游分离系统给出的说话人标签。完整的音频 scheme 参考请见源文档。
说话人分离产出什么,Potato 在其中的位置
说话人分离把一段录音切分成归属于单个说话人的片段,并为每个片段赋予一个说话人标识。它回答的是"谁在什么时候说话",只要录音里不止一个人的声音就会用到它,从会议纪要、客服中心分析,到播客制作、研究访谈和法律录音。
Potato 标注分离结果,而不执行分离。语音识别和说话人分离在你已有的系统里运行,比如 pyannote、WhisperX 或某个云端转写 API,Potato 摄入其输出交给人工检查和修正。下面两个任务覆盖随之而来的两种情况:一种是还没有分离系统的输出,另一种是已有输出但需要审核。
用波形任务标注轮次
没有上游输出可供修正时,标注者自己在波形上画出轮次。schema 是处于 label 模式的 audio_annotation,而 item_properties.text_key 指明存放音频 URL 的字段,Potato 靠它找到每条数据对应的媒体文件。
annotation_task_name: "Speaker Diarization"
task_dir: "."
data_files:
- "data/conversations.json"
item_properties:
id_key: id
text_key: audio_url
output_annotation_dir: "annotation_output/"
export_annotation_format: "jsonl"
annotation_schemes:
- annotation_type: audio_annotation
name: speakers
description: "Mark each stretch of speech and label who is speaking"
mode: label
zoom_enabled: true
playback_rate_control: true
min_segments: 1
label_requirement:
required: true
labels:
- name: Speaker 1
color: "#FF6B6B"
key_value: "1"
- name: Speaker 2
color: "#4ECDC4"
key_value: "2"
- name: Speaker 3
color: "#45B7D1"
key_value: "3"
- name: Overlap
color: "#FFEAA7"
key_value: "4"
- name: Silence
color: "#9CA3AF"
key_value: "5"
user_config:
allow_all_users: trueOverlap 和 Silence 这两个标签的作用是让时间轴保持完整。没有表示交叉说话的标签,标注者就得决定同时开口的两个人谁拥有这段区域,而两个标注者对同一秒音频会做出不同的决定。没有表示静音的标签,时间轴上的空档就在"没有人说话"和"还没有人标到这里"之间含混不清。
min_segments 只有在同时设置 label_requirement.required: true 时才生效。两者都设置后,标注者必须至少标出一个片段、并为标出的每个片段都打上标签,否则"下一条"保持锁定,提示信息会说明缺的是哪一项。
一个片段是怎么产生的
标注者有三种方式创建片段,选哪一种取决于边界需要多精确。在放大后的波形上按住右键拖动可直接画出片段,拖到边缘时视图会自动滚动,因此一段长过可见窗口的轮次仍然一次手势就能完成。在总览条上按住右键拖动,可以在录音的任意位置画出一个粗略片段,无需先导航过去。[ 和 ] 键在播放头处设定边界,Enter 创建片段,这是标注者一路听下去时最快的路径。三种方式下左键都保留它原本移动播放头的职责,所以导航和绘制不会冲突。
为每个轮次附加问题
单靠说话人标签承载不了言语行为、说话人角色或逐轮次的质量判断。设置 mode: both 会保留标签,并为每个片段附上一组问题,标注者选中某个区域后在片段面板里作答:
annotation_schemes:
- annotation_type: audio_annotation
name: turns
description: "Mark each speaker turn and describe it"
mode: both
labels:
- name: Moderator
color: "#EF4444"
key_value: "1"
- name: Participant 1
color: "#3B82F6"
key_value: "2"
- name: Participant 2
color: "#10B981"
key_value: "3"
- name: Overlap
color: "#FCD34D"
key_value: "4"
segment_schemes:
- annotation_type: radio
name: speech_type
description: "What kind of turn is this?"
labels: ["Statement", "Question", "Response", "Interruption", "Backchannel"]
- annotation_type: radio
name: speaker_confidence
description: "How sure are you about the speaker?"
labels: ["Certain", "Probable", "Guess"]片段问题带来两个限制。这些问题里不绑定键盘快捷键,因为一个片段的字段只在该片段被选中时存在,而全局按键会作用到当时恰好打开的那个片段上,所以这些答案要靠点击给出。label_requirement 在子 scheme 上同样不生效,因为把某一项标为必填会让"下一条"卡在标注者可能根本没有打开过的问题上,这些字段的完整性改为在分析阶段检查。
在难听清的音频上,这样一个 speaker_confidence 字段值得多点一次鼠标。它把标注者有把握的轮次和靠猜的轮次分开,而这正是"分歧说明你的标签集有问题"和"分歧说明录音质量差"之间的区别。
修正分离系统的输出
当分离系统已经切分好录音时,在波形上重画一遍它的边界是在浪费已有的工作。audio_dialogue 显示类型把分离后的转写稿渲染成与音频同步的说话人气泡,每个气泡都带一个只播放该轮次的播放按钮,于是标注者是在读和听,而不是在画。它读取 WhisperX 风格的分离 JSON,其中每个片段带有起点、终点、文本和说话人,也支持字幕格式;没有说话人的轮次会渲染为未分配,由标注者补上。
audio_dialogue 是一个显示类型而非标注类型,所以它写在 instance_display.fields 下,标注 scheme 仍然是独立的。绑定到该字段的 turn_level scheme 会把同一个问题放到每个轮次上:
annotation_task_name: "Diarization Review"
task_dir: "."
data_files:
- "data/interviews.json"
item_properties:
id_key: id
text_key: title
output_annotation_dir: "annotation_output/"
export_annotation_format: "jsonl"
instance_display:
fields:
- key: conversation
type: audio_dialogue
label: "Transcript"
display_options:
audio_key: audio
turns_key: turns
show_timestamps: true
speakers:
- id: interviewer
name: "Interviewer"
color: "#7c3aed"
side: left
- id: participant
name: "Participant"
color: "#059669"
side: right
annotation_schemes:
- annotation_type: radio
name: speaker_correct
description: "Is the speaker label on this turn right?"
labels: ["Correct", "Wrong speaker", "Two speakers in one turn"]
turn_level: true
turn_binding:
field: conversation
- annotation_type: radio
name: recording_quality
description: "Overall recording quality"
labels:
- "Excellent, all speakers clear"
- "Good, most speech understandable"
- "Fair, some difficulty"
- "Poor, significant problems"
user_config:
allow_all_users: truespeakers 名册为每个已知说话人固定了颜色和位置,于是一个标错说话人的轮次会以气泡出现在转写稿的错误一侧,标注者还没读一个字就能看出来。点击某个轮次的说话人姓名即可重新分配,也可以指定名册之外的说话人。逐轮次的答案是按每个轮次上稳定的 turn_id 存储的,所以如果在上游修正之后重新摄入转写稿,请保持这些标识不变。
从一个装满 ASR 输出的文件夹构建数据文件只需一条命令:
potato transcripts ./whisper_out --media-dir ./audio -o data/interviews.json让多个标注者标注同一段音频
说话人边界是标注者分歧最大的地方,而对一段录音只标一遍就没有任何可比较的东西。num_annotators_per_item 设定项目的覆盖目标:
num_annotators_per_item: 3在试点子集上安排三名标注者,足以在整批录音发出去之前看出标签集是否站得住脚。关于如何确定这个子集的规模以及如何解读结果,请见质量控制指南。
键盘快捷键
音频标注的节奏由播放控制决定,所以第一天就把快捷键交给标注者:
| Key | 操作 |
|---|---|
Space | 播放或暂停 |
Left / Right | 后退或前进 5 秒 |
Shift+Left / Shift+Right | 跳转 30 秒 |
[ / ] | 在播放头处设定片段起点 / 终点 |
Enter | 由当前选区创建片段 |
Delete | 删除选中的片段 |
1-9 | 选择标签,前提是定义了 key_value |
+ / - / 0 | 放大、缩小,或让波形适应视图 |
输出文件里有什么
片段写在 scheme 名称之下,每个片段带有自己的标识、以秒为单位的边界、标签,以及片段问题的答案:
{
"turns": {
"segments": [
{
"id": "segment_1",
"start_time": 0.0,
"end_time": 5.2,
"label": "Moderator",
"annotations": {
"speech_type": "Statement",
"speaker_confidence": "Certain"
}
},
{
"id": "segment_2",
"start_time": 5.2,
"end_time": 12.8,
"label": "Participant 1",
"annotations": {
"speech_type": "Response",
"speaker_confidence": "Certain"
}
}
]
}
}边界以秒而非采样点存储,所以同一个文件不需要转换步骤就能与任何自动语音识别(ASR)系统的转写稿对齐。每个片段还保留自己的标识,这使得第二位标注者的结果可以逐片段地与第一位比对,而不是只比对轮次数量。
长录音
在长达一小时的音频上,先撑不住的是波形渲染。安装 BBC 的 audiowaveform 工具后,Potato 就能在服务器端计算波形,而不是在标注者的浏览器里算,这是任务秒开和每条数据都卡住之间的区别:
brew install audiowaveform # macOS
sudo apt-get install audiowaveform # Ubuntu/Debian没有它,Potato 会退回到在浏览器中生成波形,这适合大约 30 分钟以内的文件。预先计算接下来的几条数据可以让标注者在条目之间不必等待:
audio_annotation:
waveform_cache_dir: waveform_cache/
waveform_look_ahead: 3
waveform_cache_max_size: 50