从 Label Studio 迁移到 Potato
如何从 Label Studio 迁移到 Potato:转换项目配置、标注 schema 和导出的数据,本指南分步讲解,并覆盖常见的标注类型。
本指南讲解如何把一个已有的 Label Studio 项目搬到 Potato 上。没有官方工具能转换 Label Studio 项目,所以配置要手工转换,数据要自己写点 Python 来重整,这需要你对两个平台都有一定了解。
功能的逐项对比见源文档。Potato 的迁移 CLI 用于升级为旧版 Potato 编写的配置,不读取 Label Studio 项目,potato import 也没有 Label Studio 格式。
为什么迁移?
对某些项目来说 Potato 更合适。它是为学术标注研究做的,自带 Prolific 和 SONA 集成,用 YAML 配置,不需要搭数据库。它用 Python 扩展起来很方便,而且因为存储就是文件,部署也很容易。
迁移概览
整个过程是手工的,大致如下:
- 手工把 Label Studio 的 XML 模板转换成 Potato 的 YAML 配置
- 写 Python 脚本转换数据格式(JSON 转 JSONL)
- 写脚本迁移已有的标注(如果有)
- 充分测试并校验转换后的数据
模板转换
文本分类
Label Studio XML:
<View>
<Text name="text" value="$text"/>
<Choices name="sentiment" toName="text" choice="single">
<Choice value="Positive"/>
<Choice value="Negative"/>
<Choice value="Neutral"/>
</Choices>
</View>Potato YAML:
annotation_task_name: "Sentiment Classification"
data_files:
- "data/items.jsonl"
item_properties:
id_key: id
text_key: text
annotation_schemes:
- annotation_type: radio
name: sentiment
description: "What is the sentiment?"
labels:
- name: positive
tooltip: "Positive sentiment"
- name: negative
tooltip: "Negative sentiment"
- name: neutral
tooltip: "Neutral sentiment"多标签分类
Label Studio XML:
<View>
<Text name="text" value="$text"/>
<Choices name="topics" toName="text" choice="multiple">
<Choice value="Politics"/>
<Choice value="Sports"/>
<Choice value="Technology"/>
<Choice value="Entertainment"/>
</Choices>
</View>Potato YAML:
annotation_schemes:
- annotation_type: multiselect
name: topics
description: "Select all relevant topics"
labels:
- name: politics
tooltip: "Politics content"
- name: sports
tooltip: "Sports content"
- name: technology
tooltip: "Technology content"
- name: entertainment
tooltip: "Entertainment content"命名实体识别
Label Studio XML:
<View>
<Labels name="entities" toName="text">
<Label value="PERSON" background="#FFC0CB"/>
<Label value="ORG" background="#90EE90"/>
<Label value="LOCATION" background="#ADD8E6"/>
</Labels>
<Text name="text" value="$text"/>
</View>Potato YAML:
annotation_schemes:
- annotation_type: span
name: entities
description: "Select entity spans in the text"
labels:
- name: PERSON
tooltip: "Person names"
- name: ORG
tooltip: "Organization names"
- name: LOCATION
tooltip: "Location names"注意:Potato 的 span 标注在高亮方式上可能和 Label Studio 不同。转换完配置后请测试一下,确认显示效果符合你的需要。
图像分类
Label Studio XML:
<View>
<Image name="image" value="$image_url"/>
<Choices name="category" toName="image">
<Choice value="Cat"/>
<Choice value="Dog"/>
<Choice value="Other"/>
</Choices>
</View>Potato YAML:
data_files:
- "data/images.jsonl"
item_properties:
id_key: id
text_key: image_url
annotation_schemes:
- annotation_type: radio
name: category
description: "What animal is in the image?"
labels:
- name: cat
tooltip: "Cat"
- name: dog
tooltip: "Dog"
- name: other
tooltip: "Other animal"边界框标注
Label Studio XML:
<View>
<Image name="image" value="$image_url"/>
<RectangleLabels name="objects" toName="image">
<Label value="Car"/>
<Label value="Person"/>
<Label value="Bicycle"/>
</RectangleLabels>
</View>Potato YAML:
annotation_schemes:
- annotation_type: image_annotation
tools: [bbox]
name: objects
description: "Draw boxes around objects"
labels:
- name: car
tooltip: "Car"
- name: person
tooltip: "Person"
- name: bicycle
tooltip: "Bicycle"注意:Potato 对边界框的支持可能和 Label Studio 有差异。当前的能力范围请查阅文档。
评分量表
Label Studio XML:
<View>
<Text name="text" value="$text"/>
<Rating name="quality" toName="text" maxRating="5"/>
</View>Potato YAML:
annotation_schemes:
- annotation_type: likert
name: quality
min_label: "Poor"
max_label: "Excellent"
description: "Rate the quality"
size: 5
labels:
- name: "1"
tooltip: "Poor"
- name: "2"
tooltip: "Below average"
- name: "3"
tooltip: "Average"
- name: "4"
tooltip: "Good"
- name: "5"
tooltip: "Excellent"数据格式转换
Label Studio JSON 到 Potato JSONL
Label Studio 格式:
[
{
"id": 1,
"data": {
"text": "This is great!",
"meta_info": "source1"
}
},
{
"id": 2,
"data": {
"text": "This is terrible.",
"meta_info": "source2"
}
}
]Potato JSONL 格式:
{"id": "1", "text": "This is great!", "metadata": {"source": "source1"}}
{"id": "2", "text": "This is terrible.", "metadata": {"source": "source2"}}转换脚本
import json
def convert_label_studio_to_potato(ls_file, potato_file):
"""Convert Label Studio JSON to Potato JSONL"""
with open(ls_file, 'r') as f:
ls_data = json.load(f)
with open(potato_file, 'w') as f:
for item in ls_data:
potato_item = {
"id": str(item["id"]),
"text": item["data"].get("text", ""),
}
# Convert nested data fields
if "data" in item:
for key, value in item["data"].items():
if key != "text":
if "metadata" not in potato_item:
potato_item["metadata"] = {}
potato_item["metadata"][key] = value
# Handle image URLs
if "image" in item.get("data", {}):
potato_item["image_url"] = item["data"]["image"]
f.write(json.dumps(potato_item) + "\n")
print(f"Converted {len(ls_data)} items")
# Usage
convert_label_studio_to_potato("label_studio_export.json", "data/items.jsonl")标注迁移
转换已有标注
def convert_annotations(ls_export, potato_output):
"""Convert Label Studio annotations to Potato format"""
with open(ls_export, 'r') as f:
ls_data = json.load(f)
with open(potato_output, 'w') as f:
for item in ls_data:
if "annotations" not in item or not item["annotations"]:
continue
for annotation in item["annotations"]:
potato_ann = {
"id": str(item["id"]),
"text": item["data"].get("text", ""),
"annotations": {},
"annotator": annotation.get("completed_by", {}).get("email", "unknown"),
"timestamp": annotation.get("created_at", "")
}
# Convert results
for result in annotation.get("result", []):
scheme_name = result.get("from_name", "unknown")
if result["type"] == "choices":
# Classification
potato_ann["annotations"][scheme_name] = result["value"]["choices"][0]
elif result["type"] == "labels":
# NER spans
if scheme_name not in potato_ann["annotations"]:
potato_ann["annotations"][scheme_name] = []
potato_ann["annotations"][scheme_name].append({
"start": result["value"]["start"],
"end": result["value"]["end"],
"label": result["value"]["labels"][0],
"text": result["value"]["text"]
})
elif result["type"] == "rating":
potato_ann["annotations"][scheme_name] = result["value"]["rating"]
f.write(json.dumps(potato_ann) + "\n")
# Usage
convert_annotations("ls_annotated_export.json", "annotations/migrated.jsonl")span 标注转换
Label Studio 用的是字符偏移量,Potato 用的也是字符偏移量,所以转换很直接:
def convert_spans(ls_spans):
"""Convert Label Studio span format to Potato format"""
potato_spans = []
for span in ls_spans:
potato_spans.append({
"start": span["value"]["start"],
"end": span["value"]["end"],
"label": span["value"]["labels"][0],
"text": span["value"]["text"]
})
return potato_spans功能映射
| Label Studio | Potato |
|---|---|
| Choices(单选) | radio |
| Choices(多选) | multiselect |
| Labels | span |
| Rating | likert |
| TextArea | text |
| RectangleLabels | image_annotation with tools: [bbox] |
| PolygonLabels | image_annotation with tools: [polygon] |
| Taxonomy | (用嵌套的 multiselect) |
| Pairwise | pairwise |
质量控制
质量控制是这次迁移真正让你有所得的地方。Label Studio 社区版完全不提供一致性指标,而标注真值、审核者分配和质量看板都在付费档位里。在 Potato 里,它们就是配置项。
注意力检查是内置的,不需要偷偷混进数据文件。Potato 会替你插入这些题目,并记录谁没通过:
attention_checks:
enabled: true
items_file: "attention_checks.json"
frequency: 10 # one check every ten items
min_response_time: 3.0 # also flag suspiciously fast answers黄金标准的工作方式相同,用你已经知道答案的条目给每位标注者打分:
gold_standards:
enabled: true
items_file: "gold_standards.json"标注者间一致性由系统替你计算。让标注者在一个共享子集上重叠,然后把它打开。Krippendorff's alpha 会出现在管理后台,你不必再离线写一段 scikit-learn:
num_annotators_per_item: 3
agreement_metrics:
enabled: true标签不确定性在 Label Studio 的任何档位都没有对应功能。Potato 会对你的标注拟合一个项目反应理论模型,为每个标签给出后验概率和置信区间,并按标注者已被证明的可靠程度对其加权,而不是简单数票:
psychometrics:
enabled: true
schema: sentiment
confidence_threshold: 0.95用户迁移
从 Label Studio 导出用户
# Label Studio API call to get users
import requests
def export_ls_users(ls_url, api_key):
response = requests.get(
f"{ls_url}/api/users",
headers={"Authorization": f"Token {api_key}"}
)
return response.json()创建 Potato 用户配置
user_config:
allow_all_users: false
users:
- username: user1@example.com
password: "..." # set a new password; Potato hashes it on first load
- username: user2@example.com
password: "..."测试迁移
校验脚本
def validate_migration(original_ls, converted_potato):
"""Validate converted data matches original"""
with open(original_ls) as f:
ls_data = json.load(f)
with open(converted_potato) as f:
potato_data = [json.loads(line) for line in f]
# Check item count
assert len(ls_data) == len(potato_data), "Item count mismatch"
# Check IDs preserved
ls_ids = {str(item["id"]) for item in ls_data}
potato_ids = {item["id"] for item in potato_data}
assert ls_ids == potato_ids, "ID mismatch"
# Check text content
for ls_item, potato_item in zip(
sorted(ls_data, key=lambda x: x["id"]),
sorted(potato_data, key=lambda x: x["id"])
):
assert ls_item["data"]["text"] == potato_item["text"], \
f"Text mismatch for item {ls_item['id']}"
print("Validation passed!")
validate_migration("label_studio_export.json", "data/items.jsonl")迁移检查清单
- 从 Label Studio 导出数据(JSON 格式)
- 手工把模板 XML 转换成 Potato YAML
- 编写并运行 Python 脚本转换数据格式(JSON 转 JSONL)
- 编写并运行脚本转换已有标注(如果有)
- 搭好 Potato 的项目结构
- 用样例数据测试
- 校验转换后的数据与原始数据一致
- 培训标注者使用新界面
- 跑一批试点标注
常见问题
有几件事容易让人栽跟头。两个工具都用 UTF-8,但还是值得检查一下数据里有没有编码上的怪东西。本地图片路径通常需要改成 URL,至少要符合 Potato 期望的格式。你在 Label Studio 里做的任何自定义组件,都得作为 Potato 的自定义模板重新做一遍。如果你写过调用 Label Studio API 的脚本,这些脚本得改成指向 Potato 的 API。