Skip to content

转写格式

Potato 能读取的所有转写与字幕格式:Whisper、WhisperX、Deepgram、AssemblyAI、AWS Transcribe、SRT、WebVTT、TTML、YouTube 字幕、CTM、Praat TextGrid 和 ELAN EAF,以及并列文件和归一化的话轮模型。

Potato 能读取 21 种转写和字幕输入,并在显示之前把它们全部转换成同一个话轮模型。无论你的语音识别流水线或字幕工具产出的是什么,都可以直接把 Potato 指过去,不用写转换脚本。 本页是关于哪些格式可用、每种格式如何被识别、各自带来什么信息的参考。分步说明见如何标注 Whisper 转写文本如何标注 YouTube 字幕

一份转写以彩色说话人气泡呈现,每个气泡带播放按钮、时间戳和话轮级标注问题。一个并列的 SRT 文件呈现为说话人气泡,每个话轮上都挂着一个问题

Potato 不做转写

语音识别说话人分离都发生在 Potato 看到任何东西之前。先运行 Whisper、WhisperX、pyannote 或某个云端 API,Potato 读取其结果。本页所写的一切都不会启动任何模型。

Note: Think-Aloud 模式确实会运行一个本地 Whisper 模型,但目的不同:它录下标注员在工作时说出的思考过程。那是一个采集功能,不是转写导入。

按内容识别,而不是按扩展名

Potato 会检查文件内部的内容,而不是相信文件名。一个叫 captions.txt 的 WebVTT 文件依然会被当作 WebVTT 解析。实际的结果是:同一份转写无论是内嵌在数据文件里,还是从磁盘上的并列文件读取,都能工作,配置不需要改动。

支持的格式

语音识别输出

格式识别依据说话人逐词时间
Whisper JSONsegments 数组有,使用 --word_timestamps
WhisperX / 已分离说话人的 JSONspeakersegments
whisper.cpp JSONtranscription 数组
Whisper TSVstart/end/text 表头
AWS Transcriberesults.itemsresults.audio_segments
Deepgramresults.channelsresults.utterances有,需 diarize=true
AssemblyAItextwords/utterances有,需 speaker_labels
Rev.aimonologues 数组
SPoRCturn_text/turnText有,推断得出

字幕与隐藏字幕

格式识别依据说话人
SubRip(.srt字幕块箭头,,mmm 分隔符来自 Name: 前缀
WebVTT(.vttWEBVTT来自 <v Name> 标签或 Name: 前缀
SubStation Alpha(.ass.ssa[Script Info] / Dialogue:来自 Name 字段
TTML / DFXP<tt> 根元素来自 speaker/agent 属性
YouTube srv1/srv2/srv3<transcript><text> XML来自 Name: 前缀
YouTube json3events 数组没有;自动字幕不带说话人

SubRipWebVTT 覆盖了你会遇到的绝大部分情况。TTML 出现在广播档案以及专业字幕工具导出的文件里。

对齐与语言学标注

格式识别依据说话人
NIST CTM空白分隔的列,数值型起始时间和时长来自声道字段
Praat TextGridFile type = "ooTextFile"每个说话人一层
ELAN EAFANNOTATION_DOCUMENT 根节点来自 PARTICIPANT,否则用层 ID

TextGrid 的长格式和短格式都能解析。对 ELAN 文件,Potato 会把 ALIGNABLE_ANNOTATIONREF_ANNOTATION 对照 TIME_ORDER 表解析,并从头部读取媒体引用。

一个 Praat TextGrid 显示为名为 fieldworker 和 speaker 的两个说话人,每层一个,静音区间被略去。TextGrid 的每一层都成为一个说话人,中间的空区间被跳过

其他

三种通用形态凑齐了这 21 种:

  • 一个 {speaker, start, end, text} 字典的简单列表。
  • 一个 {"audio": ..., "turns": [...]} 对象。
  • 完全没有时间信息的纯段落,显示为一个气泡。

不支持的格式

这些没有对应的解析器,请先转换。

  • SAMI(.smi)、MicroDVD(.sub)、SubViewer(.sbv
  • Transcriber(.trs)、EXMARaLDA、CHAT/CHILDES(.cha
  • Montreal Forced Aligner 和 Gentle 的原生输出。请从这些工具导出 CTM 或 TextGrid。
  • 包在特殊外壳里的 Azure Speech、Google Speech-to-Text 以及 Whisper API 的 verbose_json
  • 单独的 RTTM 说话人分离文件

只要来源提供,逐词置信度都会被解析并保留在数据模型中,但目前没有查看它的界面。

归一化的话轮模型

上面所有格式最终都会变成这样:

json
{
  "audio": "media/interview_01.mp3",
  "turns": [
    {
      "turn_id": "t0",
      "speaker": "host",
      "start": 0.0,
      "end": 6.5,
      "text": "Welcome back.",
      "words": [{"word": "Welcome", "start": 0.0, "end": 0.4, "confidence": 0.99}],
      "confidence": 0.97
    }
  ]
}

只有来源本身带有这些信息时,wordsconfidence 才会出现。未做说话人分离的话轮 speakernull,会显示为带选择器的 Unassigned,供标注员补上。

三个带灰色斜纹背景的转写话轮,因为源格式没有说话人标签,每个都标着 Unassigned 并带下拉箭头。YouTube 自动字幕到达时没有说话人,所以在标注员选定之前每个话轮都是 Unassigned

turn_id 是话轮级标注和说话人分配的持久化键。来源中若有显式的 turn_idstep_id 字符串就用它,否则用 t{索引}。同一个文件总是产生相同的 ID,所以标注在刷新后依然保留。

Warning: 不同工具的时间单位并不一致。Whisper 和 Deepgram 用浮点秒;AssemblyAI、whisper.cpp 的偏移量以及 Whisper 的 TSV 输出用整数毫秒。Potato 在边界处完成换算,因此之后的一切都是秒。如果你自己的预处理把两者混在一起,时间就会差 1000 倍。

并列文件

如果一个字段值是一行的短路径且以已知的转写扩展名结尾,它会被从磁盘读取,而不是当作文本。这样一来,语音识别工具本来就生成好的目录结构,即媒体文件旁边放着对应的 .srt.json,无需任何预处理步骤即可使用:

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

路径相对于 task_dir 解析,并经过与其他所有配置路径相同的路径安全校验,因此数据文件无法读取项目之外的内容。

可识别的扩展名:.srt .vtt .webvtt .json .json3 .srv1 .srv2 .srv3 .ttml .dfxp .xml .ass .ssa .tsv .ctm .TextGrid .eaf .txt

如果你的数据里确实存放着看起来像文件名的一行转写,请关闭这个启发式判断:

yaml
display_options:
  transcript_is_path: auto   # auto (default) | true | false

配置

显示方式:audio_dialogue

音频对话显示把话轮画成与音频同步的说话人气泡,每个话轮上都有播放按钮。

yaml
instance_display:
  fields:
    - key: conversation
      type: audio_dialogue
      label: "Transcript"
      span_target: true
      display_options:
        audio_key: audio
        turns_key: turns
        speaker_key: speaker
        text_key: text
        transcript_is_path: auto
        show_timestamps: true
        allow_speaker_assignment: auto
        scroll_height: 460px
选项默认值作用
audio_keyaudio字段值中存放音频 URL 或路径的子键。
turns_keyturns存放话轮列表的子键,也接受 segments
speaker_key / text_keyspeaker / text话轮级的键。
speakers[]{id, name, color, side} 名单。未列出的说话人会得到确定性的颜色和左右交替的位置。
allow_speaker_assignmentauto当存在未分离的话轮或有名单需要校正时,auto 会开启点击分配。true 强制开启,false 关闭。
transcript_is_pathauto是否把字段值当作并列文件的路径读取。
show_timestampstrue在每个话轮上显示 mm:ss–mm:ss
scroll_height480px可滚动转写区域的高度。
playback_rates[1, 1.25, 1.5, 2]速度选择器中的选项。

标注方案

从 2.7.1 起,speech_transcriptvoice_interactiontiered_annotation 都直接从实例记录中读取转写,并接受本页列出的所有格式。在此之前只有 audio_dialogue 显示方式能做到。

yaml
annotation_schemes:
  - annotation_type: speech_transcript
    name: transcript_review
    description: "Mark transcription errors"
    segments_key: segments       # record field holding the transcript
 
  - annotation_type: voice_interaction
    name: barge_in
    description: "Mark overlaps and barge-in"
    turns_key: turns

分层标注可以用转写预先填好某一层,让标注员去修正已有的对齐,而不是手工重新切分语音:

yaml
  - annotation_type: tiered_annotation
    name: tiers
    source_field: audio_url
    media_type: audio
    transcript_field: asr_output   # opt-in; omit to start from a blank timeline
    transcript_tier: utterance     # defaults to the first tier
    tiers:
      - name: utterance
        labels:
          - name: speech
            color: "#7c3aed"

预填的标注在标注员真正做出修改之前不会写入,所以只是打开过实例的人不会被记上工作量。

potato transcripts 生成数据文件

把转换工具指向一个装着语音识别输出的目录,它会写出可直接标注的数据文件:

bash
# Pair transcripts to media by basename
potato transcripts ./whisper_out --media-dir ./audio -o data/interviews.json
 
# Media served from elsewhere
potato transcripts './captions/*.vtt' \
  --media-url-prefix https://cdn.example.org/audio -o data/talks.json
 
# What did it detect? Writes nothing.
potato transcripts ./whisper_out --dry-run

--dry-run 不写任何东西,只报告它找到了什么,这也是发现目录里放错 Whisper 输出文件的最快方式:

text
Scanned 3 file(s):
  talk_01.srt          SRT                  2 turns      5.0s  2 speaker(s): Alice, Bob
  talk_02.vtt          WebVTT               1 turns      3.0s  undiarized
  talk_03.json         whisper.cpp JSON     1 turns      2.4s  undiarized

3 item(s), 4 turn(s).
1 item(s) have no media. Pass --media-dir or --media-url-prefix to enable playback.
选项用途
-o--output写到哪里。除 --dry-run 外必填。
--formatjson(默认)或 jsonl
--media-dir按文件主名配对的媒体目录。
--media-url-prefix用媒体的基准 URL 代替本地文件。
--field转写要放进条目的哪个字段。默认 conversation
--id-prefix加在每个生成 ID 前面的字符串。
--speaker-key源数据中存放说话人标签的键。
-r--recursive递归进入子目录。
--dry-run报告每个文件识别出的格式和话轮数。
--emit-config同时打印一段匹配的 config.yaml
-q--quiet关闭逐文件报告。

条目 ID 来自去掉末尾媒体扩展名后的文件名,所以 Whisper 的 interview_01.mp3.json 得到的是 interview_01,而不是 interview_01.mp3

再导出回去

分层标注可以导出为 ELAN EAF 和 Praat TextGrid,所以一份转写能走完一个来回:导入、在 Potato 里标注、导出、在 ELAN 或 Praat 里精修,再把结果读回来。

bash
python -m potato.export --config config.yaml --format eaf --output ./out/
python -m potato.export --config config.yaml --format textgrid --output ./out/

这两个导出器都以 tiered_annotation 的输出为基础。其他方案通过标准格式导出。

问题排查

现象发生了什么
全部显示为一个大气泡格式没有被识别,退回到了纯段落兜底。运行 potato transcripts <file> --dry-run,如果报告 plain text,说明时间信息根本没被解析。通常这个文件是 Whisper 的 .txt(完全没有时间信息),而不是 .json.srt
没有说话人,全是 Unassigned来源没有说话人标签。Whisper 本身不做说话人分离,YouTube 自动字幕也不做。请在上游运行说话人分离,或让标注员在界面里分配。
时间差了 1000 倍上游把秒和毫秒混用了。whisper.cpp 的 offsets、AssemblyAI 和 Whisper 的 TSV 都是毫秒。
转写路径本身显示成了正文并列文件读不出来,于是路径被当成内容显示了。检查它是否解析到 task_dir 之下。服务器日志里会记录具体原因。
一行的内嵌转写被当成文件名在显示字段上设置 transcript_is_path: false

示例项目

Potato 仓库中的 examples/audio/transcript-formats/ 并排展示六种格式,每种都从并列文件加载:SubRip、WebVTT、Whisper JSON、YouTube json3、Praat TextGrid 和 Deepgram。六种都产生相同的说话人气泡。

bash
python potato/flask_server.py start examples/audio/transcript-formats/config.yaml -p 8000

同一个任务也提供为可下载的示例设计:Transcript Format Ingestion

实现细节参见源文档

延伸阅读