feat(annotation): 支持从XML配置解析标注任务模板

- 添加 XML 配置解析功能,支持从 Label Studio XML 格式提取 objects 和 labels
- 优化模板配置加载逻辑,优先使用 configuration 字段,否则从 labelConfig 解析
- 增加对多种数据对象类型的解析支持(Image、Text、Audio 等)
- 实现标签控件类型的完整解析(Choices、Labels、RectangleLabels 等)
This commit is contained in:
2026-01-19 20:42:48 +08:00
parent 2229eb218d
commit 85b8513b43

View File

@@ -121,18 +121,29 @@ export default function CreateAnnotationTask({
});
setSelectedDatasetId(taskDetail.datasetId);
// 填充模板配置
if (taskDetail.configuration) {
const { objects, labels } = taskDetail.configuration;
// 配置可能在 template.configuration 或 taskDetail.configuration
const configuration = taskDetail.template?.configuration || taskDetail.configuration;
const labelConfig = taskDetail.template?.labelConfig || taskDetail.labelConfig;
// 设置 XML 配置用于预览
if (labelConfig) {
setCustomXml(labelConfig);
}
// 填充模板配置:优先使用 configuration,否则从 labelConfig 解析
if (configuration && configuration.objects?.length > 0) {
const { objects, labels } = configuration;
manualForm.setFieldsValue({
objects: objects || [],
labels: labels || [],
});
}
// 设置 XML 配置用于预览
if (taskDetail.labelConfig) {
setCustomXml(taskDetail.labelConfig);
} else if (labelConfig) {
// 从 XML 解析配置
const parsed = parseXmlToConfig(labelConfig);
manualForm.setFieldsValue({
objects: parsed.objects,
labels: parsed.labels,
});
}
// 编辑模式始终使用 custom 配置模式(不改变结构,只改标签)
@@ -243,6 +254,80 @@ export default function CreateAnnotationTask({
}
};
// 从 Label Studio XML 配置解析出 objects 和 labels
const parseXmlToConfig = (xml: string): { objects: any[], labels: any[] } => {
const objects: any[] = [];
const labels: any[] = [];
try {
const parser = new DOMParser();
const doc = parser.parseFromString(xml, "text/xml");
// 数据对象类型列表
const objectTypes = ["Image", "Text", "Audio", "Video", "HyperText", "Header", "Paragraphs", "TimeSeries", "TimeSeriesChannel"];
// 标签控件类型列表
const controlTypes = ["Choices", "Labels", "RectangleLabels", "PolygonLabels", "EllipseLabels", "KeyPointLabels", "BrushLabels", "TextArea", "Number", "DateTime", "Rating", "Taxonomy"];
// 解析数据对象
objectTypes.forEach(type => {
const elements = doc.getElementsByTagName(type);
for (let i = 0; i < elements.length; i++) {
const el = elements[i];
const name = el.getAttribute("name") || "";
const value = el.getAttribute("value") || "";
if (name) {
objects.push({ name, type, value });
}
}
});
// 解析标签控件
controlTypes.forEach(type => {
const elements = doc.getElementsByTagName(type);
for (let i = 0; i < elements.length; i++) {
const el = elements[i];
const fromName = el.getAttribute("name") || "";
const toName = el.getAttribute("toName") || "";
const required = el.getAttribute("required") === "true";
if (fromName) {
const label: any = {
fromName,
toName,
type,
required,
};
// 解析选项/标签值
if (type === "Choices") {
const choices: string[] = [];
const choiceElements = el.getElementsByTagName("Choice");
for (let j = 0; j < choiceElements.length; j++) {
const value = choiceElements[j].getAttribute("value");
if (value) choices.push(value);
}
label.options = choices;
} else if (["Labels", "RectangleLabels", "PolygonLabels", "EllipseLabels", "KeyPointLabels", "BrushLabels"].includes(type)) {
const labelValues: string[] = [];
const labelElements = el.getElementsByTagName("Label");
for (let j = 0; j < labelElements.length; j++) {
const value = labelElements[j].getAttribute("value");
if (value) labelValues.push(value);
}
label.labels = labelValues;
}
labels.push(label);
}
}
});
} catch (e) {
console.error("Failed to parse XML config:", e);
}
return { objects, labels };
};
const generateXmlFromConfig = (objects: any[], labels: any[]) => {
let xml = '<View>\n';