转写格式
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 JSON | segments 数组 | 无 | 有,使用 --word_timestamps 时 |
| WhisperX / 已分离说话人的 JSON | 带 speaker 的 segments | 有 | 有 |
| whisper.cpp JSON | transcription 数组 | 无 | 无 |
| Whisper TSV | start/end/text 表头 | 无 | 无 |
| AWS Transcribe | results.items 或 results.audio_segments | 有 | 有 |
| Deepgram | results.channels 或 results.utterances | 有,需 diarize=true | 有 |
| AssemblyAI | text 加 words/utterances | 有,需 speaker_labels | 有 |
| Rev.ai | monologues 数组 | 有 | 有 |
| SPoRC | turn_text/turnText 行 | 有,推断得出 | 无 |
字幕与隐藏字幕
| 格式 | 识别依据 | 说话人 |
|---|---|---|
SubRip(.srt) | 字幕块箭头,,mmm 分隔符 | 来自 Name: 前缀 |
WebVTT(.vtt) | WEBVTT 头 | 来自 <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 json3 | events 数组 | 没有;自动字幕不带说话人 |
SubRip 和 WebVTT 覆盖了你会遇到的绝大部分情况。TTML 出现在广播档案以及专业字幕工具导出的文件里。
对齐与语言学标注
| 格式 | 识别依据 | 说话人 |
|---|---|---|
| NIST CTM | 空白分隔的列,数值型起始时间和时长 | 来自声道字段 |
| Praat TextGrid | File type = "ooTextFile" | 每个说话人一层 |
| ELAN EAF | ANNOTATION_DOCUMENT 根节点 | 来自 PARTICIPANT,否则用层 ID |
TextGrid 的长格式和短格式都能解析。对 ELAN 文件,Potato 会把 ALIGNABLE_ANNOTATION 和 REF_ANNOTATION 对照 TIME_ORDER 表解析,并从头部读取媒体引用。
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 说话人分离文件
只要来源提供,逐词置信度都会被解析并保留在数据模型中,但目前没有查看它的界面。
归一化的话轮模型
上面所有格式最终都会变成这样:
{
"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
}
]
}只有来源本身带有这些信息时,words 和 confidence 才会出现。未做说话人分离的话轮 speaker 为 null,会显示为带选择器的 Unassigned,供标注员补上。
YouTube 自动字幕到达时没有说话人,所以在标注员选定之前每个话轮都是 Unassigned
turn_id 是话轮级标注和说话人分配的持久化键。来源中若有显式的 turn_id 或 step_id 字符串就用它,否则用 t{索引}。同一个文件总是产生相同的 ID,所以标注在刷新后依然保留。
Warning: 不同工具的时间单位并不一致。Whisper 和 Deepgram 用浮点秒;AssemblyAI、whisper.cpp 的偏移量以及 Whisper 的 TSV 输出用整数毫秒。Potato 在边界处完成换算,因此之后的一切都是秒。如果你自己的预处理把两者混在一起,时间就会差 1000 倍。
并列文件
如果一个字段值是一行的短路径且以已知的转写扩展名结尾,它会被从磁盘读取,而不是当作文本。这样一来,语音识别工具本来就生成好的目录结构,即媒体文件旁边放着对应的 .srt 或 .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
如果你的数据里确实存放着看起来像文件名的一行转写,请关闭这个启发式判断:
display_options:
transcript_is_path: auto # auto (default) | true | false配置
显示方式:audio_dialogue
音频对话显示把话轮画成与音频同步的说话人气泡,每个话轮上都有播放按钮。
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_key | audio | 字段值中存放音频 URL 或路径的子键。 |
turns_key | turns | 存放话轮列表的子键,也接受 segments。 |
speaker_key / text_key | speaker / text | 话轮级的键。 |
speakers | [] | {id, name, color, side} 名单。未列出的说话人会得到确定性的颜色和左右交替的位置。 |
allow_speaker_assignment | auto | 当存在未分离的话轮或有名单需要校正时,auto 会开启点击分配。true 强制开启,false 关闭。 |
transcript_is_path | auto | 是否把字段值当作并列文件的路径读取。 |
show_timestamps | true | 在每个话轮上显示 mm:ss–mm:ss。 |
scroll_height | 480px | 可滚动转写区域的高度。 |
playback_rates | [1, 1.25, 1.5, 2] | 速度选择器中的选项。 |
标注方案
从 2.7.1 起,speech_transcript、voice_interaction 和 tiered_annotation 都直接从实例记录中读取转写,并接受本页列出的所有格式。在此之前只有 audio_dialogue 显示方式能做到。
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分层标注可以用转写预先填好某一层,让标注员去修正已有的对齐,而不是手工重新切分语音:
- 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 生成数据文件
把转换工具指向一个装着语音识别输出的目录,它会写出可直接标注的数据文件:
# 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 输出文件的最快方式:
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 外必填。 |
--format | json(默认)或 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 里精修,再把结果读回来。
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。六种都产生相同的说话人气泡。
python potato/flask_server.py start examples/audio/transcript-formats/config.yaml -p 8000同一个任务也提供为可下载的示例设计:Transcript Format Ingestion。
实现细节参见源文档。
延伸阅读
- 如何标注 Whisper 转写文本,完整流程
- 如何标注 YouTube 字幕,字幕及其限制
- 对话标注,说话人气泡显示方式
- 音频标注,从零开始的波形切分
- 为标注设计数据格式