如何标注 Whisper 转写文本
如何把 Whisper 或 WhisperX 的输出直接变成一个可运行的标注项目:该保留哪个输出文件、什么时候需要说话人分离、怎样分配说话人,以及如何在导出时保留时间对齐。
Whisper 给出的是带时间的片段,标注工具本来就该直接读它。保留 .json 输出而不是 .txt,需要说话人标签就跑一遍 WhisperX,然后把 Potato 指向那个目录即可。 不需要转换脚本,也不需要重新排版。逐个格式的细节见转写格式。
Whisper 是 OpenAI 的开源语音识别模型,已经成为手上有一堆音频时默认的第一步。它产出的是一组带时间的片段,标注项目需要的东西大半都在里面。缺的是从"我有转写文本"到"标注员正在打标签"之间的所有环节。
Note: Potato 不做转写。语音识别在上游运行,Potato 读取它的输出。本文说明上游的哪些选择会产生影响,但转写本身在 Potato 看到任何东西之前就已经完成了。
Whisper 的哪个输出文件该保留?
Whisper 会写出好几个文件,选哪个比看上去更重要:
whisper interview_01.mp3 --model medium --output_format json --word_timestamps True| 文件 | 内容 | 能用吗 |
|---|---|---|
.json | 带起止时间的片段,可选逐词时间 | 用这个 |
.srt / .vtt | 带时间的片段,没有元数据 | 可以,工作正常 |
.tsv | 毫秒为单位的起止时间加文本 | 可以 |
.txt | 只有文本,没有时间信息 | 不行,没有任何可与音频同步的依据 |
如果只留下了 .txt,对齐信息就已经丢了,不重新跑模型就找不回来。转写在 Potato 里显示成一整块不分段的文字,绝大多数情况都是这个原因。
Whisper 会标注说话人吗?
不会,这一点经常出乎意料。Whisper 只做转写,不告诉你是谁在说话。所有话轮都以未分配的状态到达,做内容标注没问题,做任何与说话人相关的事情就很麻烦。
说话人分离是另一个环节。有三种处理方式。
运行 WhisperX,它在 Whisper 外面包了一层 pyannote 的说话人分离:
whisperx interview_01.mp3 --model medium --diarize --output_format json它的输出里每个片段都带 speaker 字段(SPEAKER_00、SPEAKER_01 等),Potato 直接读取。
使用开启了说话人分离的云端 API。 带 diarize=true 的 Deepgram、带 speaker_labels 的 AssemblyAI、AWS Transcribe 和 Rev.ai 都会输出说话人标签,Potato 四种都能原生读取。
未做说话人分离的话轮会显示为 Unassigned,并带有说话人选择器
交给标注员。 话轮未做说话人分离时,每个气泡会显示为带选择器的 Unassigned。在音质差、发言重叠或背景嘈杂的情况下,人耳判断往往比自动分离更准。对小规模语料来说,这是一个合理的取舍,而不是退而求其次。
数据文件怎么生成?
把转换工具指向你的 Whisper 输出目录:
potato transcripts ./whisper_out --media-dir ./audio -o data/interviews.json转写按文件主名与媒体配对,所以 interview_01.json 会找到 interview_01.mp3。条目 ID 取自文件名,Whisper 那种 interview_01.mp3.json 的双扩展名也会被正确处理,不会生成 interview_01.mp3 这样的 ID。
在往下走之前,先确认它读懂了什么:
potato transcripts ./whisper_out --dry-runScanned 3 file(s):
interview_01.json Whisper JSON 42 turns 891.4s undiarized
interview_02.json WhisperX JSON 51 turns 1120.8s 2 speaker(s): SPEAKER_00, SPEAKER_01
interview_03.json Whisper JSON 38 turns 754.2s undiarized
3 item(s), 131 turn(s).
报告为 plain text 或话轮数为零的文件就是有问题的那个。几乎总是混进 .json 输出里的一个 .txt。
能跳过转换这一步吗?
可以。数据文件可以直接指向磁盘上的转写文件:
{"id": "int_001", "conversation": {"audio": "media/int_001.mp3",
"transcript": "media/int_001.srt"}}Potato 会读取文件、根据内容判断格式,并在渲染实例时完成归一化。你的转写仍然是可以做差分、可以再导出的文件,而不会被烘进一坨数据里。
配置长什么样?
生成的数据文件是这个形状:
{
"id": "interview_01",
"conversation": {
"audio": "audio/interview_01.mp3",
"turns": [
{"turn_id": "t0", "speaker": "SPEAKER_00", "start": 0.0, "end": 6.5,
"text": "Welcome back."}
]
}
}对应的配置由 potato transcripts --emit-config 直接打印出来:
annotation_task_name: "Interview Annotation"
task_dir: .
data_files:
- data/interviews.json
item_properties:
id_key: id
text_key: conversation
instance_display:
fields:
- key: conversation
type: audio_dialogue
label: "Transcript"
span_target: true
display_options:
show_timestamps: true
allow_speaker_assignment: auto
annotation_schemes:
- annotation_type: span
name: topics
description: "Highlight topic mentions"
target_field: conversation
labels:
- name: policy
- name: personal
audio_dialogue 显示方式,每个话轮上都挂着一个话轮级问题
audio_dialogue 会把话轮画成与音频同步的说话人气泡,每个话轮上的播放按钮只播放该话轮。span_target: true 让跨度可以跨越话轮边界,并且在重新分配说话人后偏移量依然稳定。
如果你更希望标注员核查转写本身而不是给内容打标签,speech_transcript 提供带错误标签和修改框的片段卡片。要研究重叠和插话,voice_interaction 提供双轨时间线。两者读的是同一批文件。
标注员怎么分配说话人?
定义一份名单,让说话人的名称、颜色和左右位置保持稳定:
display_options:
allow_speaker_assignment: auto
speakers:
- id: interviewer
name: "Interviewer"
color: "#7c3aed"
side: left
- id: participant
name: "Participant"
color: "#059669"
side: right当存在需要分配的未分离话轮时,auto 会打开选择器。如果源数据本来就带说话人标签,但你仍想允许重新分配,就设成 true;修正说话人分离错误而不是填空时需要这样。
分配结果会与标注一起保存,并绑定在稳定的话轮 ID 上,刷新后依然保留。
标注怎么取出来?
导出为 JSON、JSONL 和 CSV 与平常一样。需要保留时间对齐时,把分层标注导出为 ELAN EAF 或 Praat TextGrid:
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 当作输入读取,所以你可以在 Potato 里标注,在 ELAN 或 Praat 里精修,再把结果读回来。
延伸阅读
- 转写格式,所有受支持的格式及其判定方式
- 如何标注 YouTube 字幕,同一流程的字幕文件版本
- 音频标注,不从转写出发的波形工作
- Transcript Format Ingestion,把六种格式并排展示的可运行示例
- 标注者间一致性,用于衡量话轮级标签的一致程度