Skip to content

如何标注 Whisper 转写文本

如何把 Whisper 或 WhisperX 的输出直接变成一个可运行的标注项目:该保留哪个输出文件、什么时候需要说话人分离、怎样分配说话人,以及如何在导出时保留时间对齐。

Whisper 给出的是带时间的片段,标注工具本来就该直接读它。保留 .json 输出而不是 .txt,需要说话人标签就跑一遍 WhisperX,然后把 Potato 指向那个目录即可。 不需要转换脚本,也不需要重新排版。逐个格式的细节见转写格式

Whisper 是 OpenAI 的开源语音识别模型,已经成为手上有一堆音频时默认的第一步。它产出的是一组带时间的片段,标注项目需要的东西大半都在里面。缺的是从"我有转写文本"到"标注员正在打标签"之间的所有环节。

Note: Potato 不做转写。语音识别在上游运行,Potato 读取它的输出。本文说明上游的哪些选择会产生影响,但转写本身在 Potato 看到任何东西之前就已经完成了。

Whisper 的哪个输出文件该保留?

Whisper 会写出好几个文件,选哪个比看上去更重要:

bash
whisper interview_01.mp3 --model medium --output_format json --word_timestamps True
文件内容能用吗
.json带起止时间的片段,可选逐词时间用这个
.srt / .vtt带时间的片段,没有元数据可以,工作正常
.tsv毫秒为单位的起止时间加文本可以
.txt只有文本,没有时间信息不行,没有任何可与音频同步的依据

如果只留下了 .txt,对齐信息就已经丢了,不重新跑模型就找不回来。转写在 Potato 里显示成一整块不分段的文字,绝大多数情况都是这个原因。

Whisper 会标注说话人吗?

不会,这一点经常出乎意料。Whisper 只做转写,不告诉你是谁在说话。所有话轮都以未分配的状态到达,做内容标注没问题,做任何与说话人相关的事情就很麻烦。

说话人分离是另一个环节。有三种处理方式。

运行 WhisperX,它在 Whisper 外面包了一层 pyannote 的说话人分离:

bash
whisperx interview_01.mp3 --model medium --diarize --output_format json

它的输出里每个片段都带 speaker 字段(SPEAKER_00SPEAKER_01 等),Potato 直接读取。

使用开启了说话人分离的云端 API。diarize=true 的 Deepgram、带 speaker_labels 的 AssemblyAI、AWS Transcribe 和 Rev.ai 都会输出说话人标签,Potato 四种都能原生读取。

三个带灰色斜纹背景的转写话轮,每个都标着 Unassigned 并带有下拉箭头。未做说话人分离的话轮会显示为 Unassigned,并带有说话人选择器

交给标注员。 话轮未做说话人分离时,每个气泡会显示为带选择器的 Unassigned。在音质差、发言重叠或背景嘈杂的情况下,人耳判断往往比自动分离更准。对小规模语料来说,这是一个合理的取舍,而不是退而求其次。

数据文件怎么生成?

把转换工具指向你的 Whisper 输出目录:

bash
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。

在往下走之前,先确认它读懂了什么:

bash
potato transcripts ./whisper_out --dry-run
text
Scanned 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

能跳过转换这一步吗?

可以。数据文件可以直接指向磁盘上的转写文件:

json
{"id": "int_001", "conversation": {"audio": "media/int_001.mp3",
                                   "transcript": "media/int_001.srt"}}

Potato 会读取文件、根据内容判断格式,并在渲染实例时完成归一化。你的转写仍然是可以做差分、可以再导出的文件,而不会被烘进一坨数据里。

配置长什么样?

生成的数据文件是这个形状:

json
{
  "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 直接打印出来:

yaml
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 提供双轨时间线。两者读的是同一批文件。

标注员怎么分配说话人?

定义一份名单,让说话人的名称、颜色和左右位置保持稳定:

yaml
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:

bash
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 里标注,在 ELANPraat 里精修,再把结果读回来。

延伸阅读