跳转到内容

用于可引用数据集和审计轨迹的项目快照

把 Potato 项目的配置、数据和标注冻结在一个论文可以引用的 id 下,比较两个快照,并给读者一个可以核对的哈希值。

potato snapshot 把一个项目的配置、数据文件和标注冻结在一个 id 下,论文或数据集卡片可以直接引用这个 id。每个快照都会对这些文件和回答计算一个 SHA-256 内容哈希,任何持有该快照文件的人都可以重新计算哈希,确认自己手里的副本就是被引用的那一份。快照还会为每个标注方案保存当天的 Krippendorff's alpha,记录当时的一致性。快照功能随 Potato 2.12.0 发布。每条命令都从磁盘读取项目,无论服务器是否在运行。

论文写完之后,研究还在继续变化。新的标注者加入,代码也会在裁定之后得到修正。在质性研究中,审计轨迹(audit trail)是对数据、编码决策和分析步骤的记录,读者据此判断分析是否可信(Nowell et al., 2017)。一系列快照为编码项目的每一轮留下一条带日期、带哈希的记录,并附上每一轮的编码者间一致性。机器学习团队也可以用同样的命令为已标注的数据集做版本管理。模型卡片随后就可以写明模型训练所用标签的快照 id。

  • 命令: 子命令包括 create、list、show、diff、tag、verify 和 deposit,全部位于 potato snapshot 之下。
  • 存储: 每个快照是任务目录中 snapshots/ 下的一个文件夹,标签保存在 snapshots/tags.json 中,Zenodo 存档记录保存在 snapshots/deposits.json 中。
  • 完整性: Potato 以只读方式写入文件,并且只在每个文件都写完之后才把文件夹重命名到最终位置。
  • 共享: 管理仪表板的 Publish Dataset 页面有一张 Snapshots 卡片,两条管理 API 路由可以列出和创建快照,还可以选择把快照存档到 Zenodo 并获得 DOI。

命令

每条命令都需要项目的配置文件:

bash
potato snapshot create config.yaml --note "pilot round finished" --tag pilot
potato snapshot list config.yaml
potato snapshot show config.yaml pilot
potato snapshot diff config.yaml pilot latest
potato snapshot tag config.yaml 20261102-175533 paper-v1
potato snapshot verify config.yaml paper-v1
potato snapshot verify --dir ./downloaded-snapshot
potato snapshot deposit config.yaml paper-v1 --check

在 Potato 源码检出目录中,python potato/flask_server.py snapshot 接受相同的参数。下表列出每条命令的参数和选项:

命令参数和选项作用
createconfig、--note TEXT(默认为空)、--tag NAME、--force冻结项目。内容哈希与最新快照的哈希相同时,create 不写入任何内容,并给出提示。--force 仍会写入一个新快照。内容未变时给出的 --tag 会指向已有的快照
listconfig、--json按从新到旧列出快照,附带标签、条目数、标注者数、回答数以及备注
showconfig、ref(默认 latest)、--json打印一个快照的哈希、Potato 版本、上一个快照、存档记录以及每个方案的 alpha
diffconfig、old、new(默认 latest)、--json报告两个快照之间的变化
tagconfig、ref、name把一个名称指向某个快照。若该名称原先指向另一个快照,则会移过来
verifyconfig 和 ref(默认 latest),或 --dir FOLDER依据清单重新计算快照的哈希。使用 --dir 时检查一个装有快照文件的文件夹,不需要配置文件
depositconfig、ref(默认 latest)、--token、--production、--publish、--annotator-ids-public、--check把快照的文件上传到 Zenodo

ref 可以是 latest、完整的快照 id、标签,或者 id 或内容哈希的开头部分。前缀必须恰好匹配一个快照,有歧义的前缀会让命令停止,并列出所有匹配项。标签由 1 到 64 个字母、数字、.、_ 或 - 组成,必须以字母或数字开头,且不能是 latest。标签的用法与 git 标签相同。create 之后,Potato 只会改动存储中的两个文件:保存标签的 tags.json 和记录 Zenodo 存档的 deposits.json。create 写好快照文件夹之后,该文件夹再也不会改变。

show 打印快照的摘要行、哈希、Potato 版本以及每个方案的 alpha:

text
20261102-175504-45d41bfa [pilot]  6 items, 2 annotators, 20 answers  pilot round finished
  created       2026-11-02T17:55:04+00:00
  content hash  45d41bfad181190c161eff442c2bd3e65bd0546f3a55c197f356db4447e72c93
  bundle sha256 addd652f15ee682301c6cab6968d8aa2f0b6d05e1e5fc6748d242d07f9f07af4
  potato        2.13.1
  alpha sentiment: 0.511
  alpha aspects: 0.718

上面计数中的一个回答,是一位标注者针对一个方案对一个条目给出的标签。create 会打印新的 id 和内容哈希、配置文件之外的研究文件数量、每个作为凭据被排除的文件、配置中提到但快照没有复制的每个文件夹,以及针对任务目录之外任何文件的警告。

快照中的文件

每个快照都是任务目录中的一个文件夹 snapshots/<id>/。id 由创建时的 UTC 时间加上内容哈希的前八个字符组成,例如 20261102-175533-68e05868。文件夹中有四个文件:

文件内容
bundle.tar.gz配置、研究文件、标注输出目录以及项目的 SQLite 数据库,全部位于 project/ 之下。这些数据库是 project.sqlite(备忘录、码本、案例、打字会话和审阅流程)、datasets.sqlite 和 engine.sqlite(标注引擎)
annotations.jsonl每位标注者、每个条目、每个方案各占一行,包含该标注者保存的每个标签和跨度,按固定顺序排列。diff 读取的就是 annotations.jsonl
items.json数据文件中的条目 id,按文件中的顺序排列
manifest.jsonid、创建时间、备注、任务名称、Potato 版本、哈希、计数、一致性摘要、研究文件、被排除的文件、上一个快照的 id 以及序号

annotations.jsonl 的每一行把标签回答放在 "labels" 下,把跨度回答以 [label, start, end] 三元组的形式放在 "spans" 下。一行标签回答如下所示:

json
{"annotator": "bob", "item": "r2", "schema": "sentiment", "value": {"labels": [["neutral", "true"]]}}

研究文件包括数据文件,以及配置中任何字符串所指向的每个文件。阶段和问卷文件、注意力检查和金标准条目文件、知情同意和说明页面,以及自定义布局都算在内。Potato 还会跟踪被引用的 YAML 文件内部的引用,最多三层。manifest.json 在 project_files 下列出这些研究文件。配置中提到的每个文件夹,Potato 都列在 referenced_directories 下,但不复制,因为音频或图像文件夹可以任意大。这类文件夹中的文件不在内容哈希范围内。请另行归档这些媒体文件,并在引用快照时一并引用媒体归档。

任务目录之外的研究文件以 external/<name> 的形式放入打包文件,create 会打印一条警告。Potato 拒绝运行指向项目之外的配置,以这种方式冻结的研究无法按原样启动。请把文件移入任务目录,然后重新创建快照。

被排除的凭据

由于快照是用来引用和转交他人的,打包文件会排除三类凭据:

  • 账户名单及其密码哈希不会被收入。设置了 authentication.user_config_path 时,名单就是该键指向的文件;否则是输出目录旁边的 user_config.json,当 output_annotation_dir 以 / 结尾时则在输出目录之内。
  • 名称中含有 password、secret、token、credential、API key 或 private key 的配置键所指向的文件不会被收入。这条规则涵盖 ssl_key 以及每个以 key_file 结尾的键,例如 sp_key_file。
  • admin_api_key.txt 不会被收入。

manifest.json 在 omitted 下列出每个被排除的文件及原因,create 也会打印这份列表。被排除的文件都不在内容哈希范围内。从快照解包出来的项目没有任何账户,除非你从自己的副本中恢复名单,否则标注者需要重新注册。

内容哈希

content_hash 是对配置文件、按打包文件中的名称排列的每个研究文件、条目 id 列表以及标注行依次计算的 SHA-256 摘要。配置、任何研究文件或任何回答发生变化,哈希都会随之改变。SQLite 数据库在打包文件中,但不在哈希范围内。

数据库不计入哈希,是因为服务器运行期间会不断重写这些数据库。如果哈希随每次重写而变化,读者就无法据此判断标注是否发生了变化。QDA 模式的备忘录、码本和案例都存放在 project.sqlite 中。只修改备忘录或代码定义不会改变内容哈希,create 会报告没有任何变化。如果要冻结一次没有改动任何回答的码本修订,请加上 --force。

verify 执行三项检查:

  1. bundle.tar.gz 必须与清单中的 bundle_sha256 一致。
  2. annotations.jsonl 必须与 annotations_sha256 一致。
  3. 从打包文件中解出的配置和研究文件,与条目 id 和标注行一起计算哈希后,必须重现 content_hash。

只持有这四个文件的读者,可以用 --dir 执行同样的检查:

text
$ potato snapshot verify --dir ./downloaded-snapshot
20261102-175533-68e05868: OK
  content hash 68e058683e7088e3a344d56d7e39172b3d7d3b9b469ba9e1a4a1876b4296b4d2
  Compare it with the hash the paper or dataset card cites.

检查通过只说明这些文件与同一文件夹中的清单相符。任何其他快照也能通过它自己的检查。请把打印出的哈希与论文引用的哈希进行比对。如果文件被改动或损坏,verify 会为每个问题打印一行,例如 annotations.jsonl does not match annotations_sha256,并以状态码 1 退出。

比较两个快照

diff 逐个回答比较两个快照,并统计新增、删除和更改的回答数。回答按标注者、条目和方案进行匹配。报告还会列出新增或删除的条目和标注者,说明配置是否有变化,并给出每个方案变化前后的 alpha:

text
$ potato snapshot diff config.yaml pilot paper-v1
20261102-175504-45d41bfa -> 20261102-175533-68e05868
Items: +0 -0
Annotations: +0 -0 changed 1
  changed  bob  r2  sentiment
Alpha aspects: 0.718 -> 0.718 (+0.000)
Alpha sentiment: 0.511 -> 0.298 (-0.213)

在示例中的两个快照之间,一位标注者更改了一个回答,有六个条目的 sentiment 方案的 alpha 从 0.511 降到了 0.298。文本报告中每类最多列出 20 行。--json 以机器可读的形式给出完整计数以及同样的这些行。对于内容哈希相同的两个快照,diff 会打印 Same content.

快照中的 alpha 是 Krippendorff's alpha(名义型),计算方式与论文模式(Paper Mode)相同。多选方案按条目和标签分别计分。每个方案还会得到 Krippendorff 的一种解读:从 0.800 起为“acceptable agreement”,从 0.667 起为“tentative agreement”,低于 0.667 为“low agreement”。无法计算 alpha 的方案显示 n/a。

恢复快照

Potato 没有恢复命令。快照的 bundle.tar.gz 解包后是一个 project/ 文件夹,其中有配置、研究文件、标注输出和数据库。Potato 可以像启动其他项目一样,直接用解包后的配置启动:

bash
mkdir restored
tar -xzf snapshots/20261102-175533-68e05868/bundle.tar.gz -C restored
potato start restored/project/config.yaml -p 8000

恢复后的项目没有账户,也没有被复制的媒体文件夹。在标注者回来之前,请从你自己的归档中把 referenced_directories 下列出的文件夹复制回来。

管理仪表板中的快照

管理仪表板的 Publish Dataset 页面有一张 Snapshots 卡片。这张卡片列出项目的快照及其条目数、标注者数和回答数,并可以根据一条备注和一个可选标签创建快照。两条管理路由通过 HTTP 提供同样的功能:

路由谁可以调用该路由结果
GET /admin/api/snapshots管理 API 密钥,或有权查看管理仪表板的会话{"snapshots": [...]},按从新到旧排列,每项附带清单、标签和存档记录
POST /admin/api/snapshots管理 API 密钥,或拥有 export_data 权限的会话201 并返回新的清单。没有任何变化时返回 200,附带 "unchanged": true 和已有的快照。标签无效时返回 400,且不写入任何内容。同一快照的另一次创建正在进行时返回 409

POST 请求体接受 note、tag 和 force,均为可选:

bash
curl -H "X-API-Key: $KEY" http://localhost:8000/admin/api/snapshots
curl -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
     -d '{"note": "second coding round", "tag": "round-2"}' \
     http://localhost:8000/admin/api/snapshots

每次从 Publish Dataset 页面发布之前,Potato 都会冻结一个快照;如果没有任何变化,则复用最新的快照。发布到本地归档、Hugging Face Hub 或 Zenodo,都以同样的方式获得快照。生成的数据集卡片会在引用部分写明该快照:

This release is Potato snapshot 20261102-175533-68e05868 (content hash 68e058683e70…). Anyone holding the project folder can check that their copy is this one with potato snapshot verify config.yaml 20261102-175533-68e05868.

如果冻结失败,发布仍会继续,Potato 会以警告的形式显示失败信息。python -m potato.publish 不会冻结快照,该命令写出的数据集卡片也不会写明任何快照。通过命令行发布之前,请先运行 potato snapshot create,并自行引用快照 id。

把快照存档到 Zenodo

potato snapshot deposit 把快照的四个文件和一份简短的 README 上传到一条新的 Zenodo 记录。读者可以下载这些文件,并在下载的文件夹上运行 potato snapshot verify --dir。记录的标题、作者、许可证、关键词和描述来自配置中的 dataset_metadata 块。Potato 按存档时配置的状态读取这个块。记录的版本号就是快照 id,描述中还会加上快照 id、计数和内容哈希。请在配置中加入一个 dataset_metadata 块:

yaml
dataset_metadata:
  pretty_name: Clinic Access Interviews (coded excerpts)
  description: Interview excerpts coded for barriers to clinic access by two coders.
  license: cc-by-4.0
  keywords: [qualitative-coding, health-services]
  authors:
    - name: A. Researcher
      affiliation: University of Example
      orcid: 0000-0002-1825-0097

除非你另作要求,存档会作为未发布的草稿进入 Zenodo 沙盒:

bash
export ZENODO_TOKEN=...
potato snapshot deposit config.yaml paper-v1 --check
potato snapshot deposit config.yaml paper-v1 --annotator-ids-public
potato snapshot deposit config.yaml paper-v1 --annotator-ids-public --production --publish

第一条命令列出文件、运行下面的检查,不发送任何内容。第二条命令创建一个沙盒草稿。第三条命令在 zenodo.org 上发布记录并生成 DOI。

选项默认值作用
--tokenZENODO_TOKENZenodo 访问令牌
--production关闭(沙盒)存档到 zenodo.org,而不是 sandbox.zenodo.org
--publish关闭(草稿)发布记录并生成 DOI。草稿在发布之前持有一个预留的 DOI
--annotator-ids-public关闭确认标注者 id 可以按存储原样公开
--check关闭列出文件并运行检查,不发送任何内容

文件会严格按冻结时的样子上传,因为哈希检查依赖于文件的字节。Potato 在发送任何内容之前会运行两项检查,任何选项都无法跳过凭据检查:

  • 凭据。 Potato 会解析配置以及打包文件中除数据文件之外的所有其他 YAML、JSON、.env 或 TOML 文件。只要其中某个文件有 api_key、token、password 或 client_secret 这类键,或以 _api_key、_secret、_token 或 _password 结尾的键,且其值是 12 个字符或更长的字面量,存档就会停止。形似某个已知服务商密钥的值,无论出现在哪个文件中,包括自定义布局的 HTML 或 JavaScript,都会让存档停止。任何文件中的 PEM 私钥、无法解析的配置文件以及超过 8 MB 的配置文件,同样会让存档停止。请把每个这样的值替换为 ${ENV_VAR},或把文件移出项目,然后重新创建快照,再存档新快照。
  • 标注者 id。 Potato 会统计回答中以及打包文件里每个已保存状态中的标注者 id,列出看起来像电子邮件地址的 id,除非你传入 --annotator-ids-public,否则会停止存档。有已保存状态但没有回答的标注者也会被计入,因为已保存状态中包含知情同意和问卷回答。

存档的快照按存储原样保留标注者 id,而 python -m potato.publish 默认把标注者 id 映射为 A1 到 An。如果 id 必须保密,请使用发布工具。每次存档都会记录在 snapshots/deposits.json 中,show 会打印每次存档及其 Zenodo URL 和 DOI。一旦某个快照在 zenodo.org 上有了已发布的 DOI,任何冻结为该快照的发布,其数据集卡片都会链接到这个 DOI。

删除快照

快照的文件是只读的。先让文件可写,再删除文件夹,并从 snapshots/tags.json 中删除指向该快照的所有标签:

bash
chmod -R u+w snapshots/20261102-175504-45d41bfa
rm -rf snapshots/20261102-175504-45d41bfa

中途被打断的 create 会留下一个 <id>.partial 文件夹,任何命令都不会读取它。你可以用同样的方法删除这个文件夹。消息 Another snapshot create is writing … right now 表示两次针对相同内容的创建在同一秒内开始。两次创建中的一次会写出快照,potato snapshot list 会显示结果。

延伸阅读

有关实现细节,请参阅源文档。

参考文献

  1. Nowell, L. S., Norris, J. M., White, D. E., & Moules, N. J. (2017). Thematic Analysis: Striving to Meet the Trustworthiness Criteria. International Journal of Qualitative Methods, 16(1). doi:10.1177/1609406917733847