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