從 Label Studio 遷移到 Potato
手動轉換 Label Studio 項目、模板和標註到 Potato 格式的分步指南。
本指南幫助你手動將現有的 Label Studio 項目遷移到 Potato。遷移涉及手動轉換配置和編寫 Python 指令碼來轉換資料格式。
請注意,沒有官方遷移工具——這是一個手動過程,需要了解兩個平臺。
為什麼遷移?
Potato 在某些場景下具有優勢:
- 研究導向:專為學術標註研究設計
- 眾包整合:原生 Prolific 和 MTurk 整合
- 簡潔性:基於 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
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 (single) | radio |
| Choices (multiple) | multiselect |
| Labels | span |
| Rating | likert |
| TextArea | text |
| RectangleLabels | bounding_box |
| PolygonLabels | polygon |
| Taxonomy | (使用巢狀 multiselect) |
| Pairwise | comparison |
品質控制
遷移到這一步,你是得到了東西,而不是失去。Label Studio 的社群版根本不含任何一致性指標,標準答案標記、審閱者分配和品質看板都在付費版裡。在 Potato 裡,它們只是配置項。
注意力檢查是內建功能,不需要你偷偷塞進資料檔案。Potato 會替你插入,並記錄誰沒通過:
attention_checks:
enabled: true
items_file: "attention_checks.json"
frequency: 10
min_response_time: 3.0標準答案同理,用你已知答案的條目給每位標註員打分:
gold_standards:
enabled: true
items_file: "gold_standards.json"標註者間一致性由系統算好。讓標註員在一個共同子集上重疊,然後開啟開關。Krippendorff α 會出現在管理面板裡,你不用再寫任何 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:
# Simple auth for migrated users
auth_type: password
user_config:
- username: user1@example.com
password_hash: "..." # Generate new passwords
- username: user2@example.com
password_hash: "..."測試遷移
驗證指令碼
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 項目結構
- 使用示例資料測試
- 驗證轉換後的資料與原始資料匹配
- 培訓標註者使用新介面
- 執行試標註批次
常見問題
字元編碼
Label Studio 和 Potato 都使用 UTF-8,但請檢查資料中的編碼問題。
影像路徑
將本地路徑轉換為 URL,或更新路徑以匹配 Potato 的預期格式。
自定義元件
Label Studio 自定義元件需要重新建立為 Potato 自定義模板。
API 差異
如果你自動化了 Label Studio,請更新指令碼以使用 Potato 的 API。