You've already forked DataMate
feat(annotation): 替换模板配置表单为树形编辑器组件
- 移除 TemplateConfigurationForm 组件并引入 TemplateConfigurationTreeEditor - 使用 useTagConfig Hook 获取标签配置 - 将自定义XML状态 customXml 替换为 labelConfig - 删除模板编辑标签页和选择模板状态管理 - 更新XML解析逻辑支持更多对象和标注控件类型 - 添加配置验证功能确保至少包含数据对象和标注控件 - 在模板详情页面使用树形编辑器显示配置详情 - 更新任务创建页面集成新的树形配置编辑器 - 调整预览数据生成功能适配新的XML解析方式
This commit is contained in:
@@ -23,11 +23,6 @@ objects:
|
||||
required_attrs: [name, value]
|
||||
optional_attrs: []
|
||||
category: document
|
||||
ParagraphLabels:
|
||||
description: "Display paragraphs with label support"
|
||||
required_attrs: [name, value]
|
||||
optional_attrs: []
|
||||
category: text
|
||||
Timeseries:
|
||||
description: "Display timeseries data"
|
||||
required_attrs: [name, value]
|
||||
@@ -118,7 +113,7 @@ controls:
|
||||
default: 3
|
||||
description: "Maximum depth of taxonomy tree"
|
||||
requires_children: true
|
||||
child_tag: Path
|
||||
child_tag: Choice
|
||||
child_required_attrs: [value]
|
||||
category: labeling
|
||||
|
||||
@@ -135,7 +130,7 @@ controls:
|
||||
requires_children: true
|
||||
child_tag: Choice
|
||||
child_required_attrs: [value]
|
||||
category: layout
|
||||
category: labeling
|
||||
|
||||
List:
|
||||
description: "List selection control"
|
||||
@@ -150,11 +145,11 @@ controls:
|
||||
requires_children: true
|
||||
child_tag: Item
|
||||
child_required_attrs: [value]
|
||||
category: layout
|
||||
category: labeling
|
||||
|
||||
Filter:
|
||||
description: "Filter control for annotation"
|
||||
required_attrs: [name, toName]
|
||||
required_attrs: []
|
||||
optional_attrs:
|
||||
required:
|
||||
type: boolean
|
||||
@@ -163,7 +158,7 @@ controls:
|
||||
|
||||
Collapse:
|
||||
description: "Collapsible UI section"
|
||||
required_attrs: [name]
|
||||
required_attrs: []
|
||||
optional_attrs:
|
||||
collapsed:
|
||||
type: boolean
|
||||
@@ -173,18 +168,18 @@ controls:
|
||||
|
||||
Header:
|
||||
description: "Section header for UI grouping"
|
||||
required_attrs: [name]
|
||||
required_attrs: [value]
|
||||
optional_attrs:
|
||||
level:
|
||||
size:
|
||||
type: number
|
||||
default: 1
|
||||
description: "Header level (1-6)"
|
||||
default: 3
|
||||
description: "Header size"
|
||||
requires_children: false
|
||||
category: layout
|
||||
|
||||
Shortcut:
|
||||
description: "Keyboard shortcut definition"
|
||||
required_attrs: [name, toName]
|
||||
required_attrs: []
|
||||
optional_attrs:
|
||||
key:
|
||||
type: string
|
||||
@@ -194,11 +189,8 @@ controls:
|
||||
|
||||
Style:
|
||||
description: "Custom style for annotation UI"
|
||||
required_attrs: [name]
|
||||
optional_attrs:
|
||||
value:
|
||||
type: string
|
||||
description: "CSS style value"
|
||||
required_attrs: []
|
||||
optional_attrs: {}
|
||||
requires_children: false
|
||||
category: layout
|
||||
|
||||
@@ -247,23 +239,14 @@ controls:
|
||||
child_required_attrs: [value]
|
||||
category: labeling
|
||||
|
||||
Relation:
|
||||
description: "Draw relation between objects"
|
||||
required_attrs: [name, toName]
|
||||
optional_attrs:
|
||||
required:
|
||||
type: boolean
|
||||
requires_children: false
|
||||
category: layout
|
||||
|
||||
Relations:
|
||||
description: "Draw multiple relations between objects"
|
||||
required_attrs: [name, toName]
|
||||
optional_attrs:
|
||||
required:
|
||||
type: boolean
|
||||
requires_children: false
|
||||
category: layout
|
||||
required_attrs: []
|
||||
optional_attrs: {}
|
||||
requires_children: true
|
||||
child_tag: Relation
|
||||
child_required_attrs: [value]
|
||||
category: labeling
|
||||
|
||||
Pairwise:
|
||||
description: "Pairwise comparison control"
|
||||
@@ -272,7 +255,7 @@ controls:
|
||||
required:
|
||||
type: boolean
|
||||
requires_children: false
|
||||
category: layout
|
||||
category: labeling
|
||||
|
||||
DateTime:
|
||||
description: "Date and time input"
|
||||
@@ -350,6 +333,15 @@ controls:
|
||||
child_required_attrs: [value]
|
||||
category: labeling
|
||||
|
||||
HyperTextLabels:
|
||||
description: "Labels for hypertext entities"
|
||||
required_attrs: [name, toName]
|
||||
optional_attrs: [required]
|
||||
requires_children: true
|
||||
child_tag: Label
|
||||
child_required_attrs: [value]
|
||||
category: labeling
|
||||
|
||||
KeyPointLabels:
|
||||
description: "Keypoint annotations with labels"
|
||||
required_attrs: [name, toName]
|
||||
|
||||
@@ -35,7 +35,7 @@ async def create_template(
|
||||
- **description**: 模板描述(可选,最多500字符)
|
||||
- **dataType**: 数据类型(必填)
|
||||
- **labelingType**: 标注类型(必填)
|
||||
- **configuration**: 标注配置(必填,包含labels和objects)
|
||||
- **labelConfig**: Label Studio XML 配置(必填)
|
||||
- **style**: 样式配置(默认horizontal)
|
||||
- **category**: 模板分类(默认custom)
|
||||
"""
|
||||
|
||||
@@ -45,7 +45,7 @@ class CreateAnnotationTemplateRequest(BaseModel):
|
||||
description: Optional[str] = Field(None, max_length=500, description="模板描述")
|
||||
data_type: str = Field(alias="dataType", description="数据类型")
|
||||
labeling_type: str = Field(alias="labelingType", description="标注类型")
|
||||
configuration: TemplateConfiguration = Field(..., description="标注配置")
|
||||
label_config: str = Field(alias="labelConfig", description="Label Studio XML 配置")
|
||||
style: str = Field(default="horizontal", description="样式配置")
|
||||
category: str = Field(default="custom", description="模板分类")
|
||||
|
||||
@@ -58,7 +58,7 @@ class UpdateAnnotationTemplateRequest(BaseModel):
|
||||
description: Optional[str] = Field(None, max_length=500, description="模板描述")
|
||||
data_type: Optional[str] = Field(None, alias="dataType", description="数据类型")
|
||||
labeling_type: Optional[str] = Field(None, alias="labelingType", description="标注类型")
|
||||
configuration: Optional[TemplateConfiguration] = Field(None, description="标注配置")
|
||||
label_config: Optional[str] = Field(None, alias="labelConfig", description="Label Studio XML 配置")
|
||||
style: Optional[str] = Field(None, description="样式配置")
|
||||
category: Optional[str] = Field(None, description="模板分类")
|
||||
|
||||
@@ -72,8 +72,8 @@ class AnnotationTemplateResponse(BaseModel):
|
||||
description: Optional[str] = Field(None, description="模板描述")
|
||||
data_type: str = Field(alias="dataType", description="数据类型")
|
||||
labeling_type: str = Field(alias="labelingType", description="标注类型")
|
||||
configuration: TemplateConfiguration = Field(..., description="标注配置")
|
||||
label_config: Optional[str] = Field(None, alias="labelConfig", description="生成的Label Studio XML配置")
|
||||
configuration: Optional[TemplateConfiguration] = Field(None, description="标注配置")
|
||||
label_config: Optional[str] = Field(None, alias="labelConfig", description="Label Studio XML配置")
|
||||
style: str = Field(..., description="样式配置")
|
||||
category: str = Field(..., description="模板分类")
|
||||
built_in: bool = Field(alias="builtIn", description="是否内置模板")
|
||||
|
||||
@@ -114,28 +114,20 @@ class AnnotationTemplateService:
|
||||
Returns:
|
||||
创建的模板响应
|
||||
"""
|
||||
# 验证配置JSON
|
||||
config_dict = request.configuration.model_dump(mode='json', by_alias=False)
|
||||
valid, error = LabelStudioConfigValidator.validate_configuration_json(config_dict)
|
||||
if not valid:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid configuration: {error}")
|
||||
|
||||
# 生成Label Studio XML配置(用于验证,但不存储)
|
||||
label_config = self.generate_label_studio_config(request.configuration)
|
||||
|
||||
# 验证生成的XML
|
||||
label_config = request.label_config
|
||||
valid, error = LabelStudioConfigValidator.validate_xml(label_config)
|
||||
if not valid:
|
||||
raise HTTPException(status_code=400, detail=f"Generated XML is invalid: {error}")
|
||||
|
||||
# 创建模板对象(不包含label_config字段)
|
||||
raise HTTPException(status_code=400, detail=f"Invalid labelConfig: {error}")
|
||||
|
||||
# 创建模板对象
|
||||
template = AnnotationTemplate(
|
||||
id=str(uuid4()),
|
||||
name=request.name,
|
||||
description=request.description,
|
||||
data_type=request.data_type,
|
||||
labeling_type=request.labeling_type,
|
||||
configuration=config_dict,
|
||||
configuration=None,
|
||||
label_config=label_config,
|
||||
style=request.style,
|
||||
category=request.category,
|
||||
built_in=False,
|
||||
@@ -280,24 +272,11 @@ class AnnotationTemplateService:
|
||||
update_data = request.model_dump(exclude_unset=True, by_alias=False)
|
||||
|
||||
for field, value in update_data.items():
|
||||
if field == 'configuration' and value is not None:
|
||||
# 验证配置JSON
|
||||
config = value if isinstance(value, TemplateConfiguration) else TemplateConfiguration.model_validate(value)
|
||||
config_dict = config.model_dump(mode='json', by_alias=False)
|
||||
valid, error = LabelStudioConfigValidator.validate_configuration_json(config_dict)
|
||||
if field == "label_config" and value is not None:
|
||||
valid, error = LabelStudioConfigValidator.validate_xml(value)
|
||||
if not valid:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid configuration: {error}")
|
||||
|
||||
# 重新生成Label Studio XML配置(用于验证)
|
||||
label_config = self.generate_label_studio_config(config)
|
||||
|
||||
# 验证生成的XML
|
||||
valid, error = LabelStudioConfigValidator.validate_xml(label_config)
|
||||
if not valid:
|
||||
raise HTTPException(status_code=400, detail=f"Generated XML is invalid: {error}")
|
||||
|
||||
# 只更新configuration字段,不存储label_config
|
||||
setattr(template, field, config_dict)
|
||||
raise HTTPException(status_code=400, detail=f"Invalid labelConfig: {error}")
|
||||
setattr(template, field, value)
|
||||
else:
|
||||
setattr(template, field, value)
|
||||
|
||||
@@ -350,20 +329,17 @@ class AnnotationTemplateService:
|
||||
Returns:
|
||||
模板响应对象
|
||||
"""
|
||||
# 将配置JSON转换为TemplateConfiguration对象
|
||||
from typing import cast, Dict, Any
|
||||
config_dict = cast(Dict[str, Any], template.configuration)
|
||||
config = TemplateConfiguration(**config_dict)
|
||||
config = None
|
||||
if template.configuration:
|
||||
try:
|
||||
from typing import cast, Dict, Any
|
||||
config_dict = cast(Dict[str, Any], template.configuration)
|
||||
config = TemplateConfiguration(**config_dict)
|
||||
except Exception:
|
||||
config = None
|
||||
|
||||
# 优先使用预定义的 label_config,否则动态生成
|
||||
if template.label_config:
|
||||
label_config = template.label_config
|
||||
else:
|
||||
label_config = self.generate_label_studio_config(config)
|
||||
|
||||
# 使用model_validate从ORM对象创建响应对象
|
||||
response = AnnotationTemplateResponse.model_validate(template)
|
||||
response.configuration = config
|
||||
response.label_config = label_config # type: ignore
|
||||
response.label_config = template.label_config # type: ignore
|
||||
|
||||
return response
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Label Studio Configuration Validation Utilities
|
||||
"""
|
||||
from typing import Dict, List, Tuple, Optional
|
||||
from typing import Dict, List, Tuple, Optional, Set
|
||||
import xml.etree.ElementTree as ET
|
||||
from app.module.annotation.config import LabelStudioTagConfig
|
||||
|
||||
@@ -13,6 +13,23 @@ class LabelStudioConfigValidator:
|
||||
def _get_config() -> LabelStudioTagConfig:
|
||||
"""获取标签配置实例"""
|
||||
return LabelStudioTagConfig()
|
||||
|
||||
@staticmethod
|
||||
def _get_required_attrs(tag_type: str, is_control: bool) -> List[str]:
|
||||
config = LabelStudioConfigValidator._get_config()
|
||||
tag_config = (
|
||||
config.get_control_config(tag_type)
|
||||
if is_control
|
||||
else config.get_object_config(tag_type)
|
||||
)
|
||||
required_attrs = tag_config.get("required_attrs", []) if tag_config else []
|
||||
return required_attrs if isinstance(required_attrs, list) else []
|
||||
|
||||
@staticmethod
|
||||
def _get_control_category(tag_type: str) -> Optional[str]:
|
||||
config = LabelStudioConfigValidator._get_config()
|
||||
tag_config = config.get_control_config(tag_type) or {}
|
||||
return tag_config.get("category")
|
||||
|
||||
@staticmethod
|
||||
def validate_xml(xml_string: str) -> Tuple[bool, Optional[str]]:
|
||||
@@ -33,24 +50,49 @@ class LabelStudioConfigValidator:
|
||||
if root.tag != 'View':
|
||||
return False, "Root element must be <View>"
|
||||
|
||||
# 检查是否有对象定义
|
||||
object_types = config.get_object_types()
|
||||
objects = [elem for elem in root.iter() if elem is not root and elem.tag in object_types]
|
||||
control_types = config.get_control_types()
|
||||
|
||||
objects = [elem for elem in root.iter() if elem.tag in object_types]
|
||||
controls = [elem for elem in root.iter() if elem.tag in control_types]
|
||||
|
||||
labeling_controls = [
|
||||
control
|
||||
for control in controls
|
||||
if LabelStudioConfigValidator._get_control_category(control.tag) == "labeling"
|
||||
]
|
||||
|
||||
if not objects:
|
||||
return False, "No data objects (Image, Text, etc.) found"
|
||||
|
||||
# 检查是否有控件定义
|
||||
control_types = config.get_control_types()
|
||||
controls = [elem for elem in root.iter() if elem is not root and elem.tag in control_types]
|
||||
if not controls:
|
||||
return False, "No annotation controls found"
|
||||
|
||||
# 验证每个控件
|
||||
|
||||
if not labeling_controls:
|
||||
return False, "No labeling controls found"
|
||||
|
||||
object_names = {
|
||||
obj.get("name") for obj in objects if obj.get("name")
|
||||
}
|
||||
|
||||
# 校验对象必填属性
|
||||
for obj in objects:
|
||||
required_attrs = LabelStudioConfigValidator._get_required_attrs(
|
||||
obj.tag, is_control=False
|
||||
)
|
||||
for attr in required_attrs:
|
||||
if not obj.attrib.get(attr):
|
||||
return False, f"Object {obj.tag} missing '{attr}' attribute"
|
||||
if obj.attrib.get("value") and not obj.attrib.get("value", "").startswith("$"):
|
||||
return False, "Object value must start with '$' (e.g., '$image')"
|
||||
|
||||
# 校验控件(布局类仅提示,不作为失败条件)
|
||||
for control in controls:
|
||||
valid, error = LabelStudioConfigValidator._validate_control(control)
|
||||
if not valid:
|
||||
category = LabelStudioConfigValidator._get_control_category(control.tag)
|
||||
strict = category == "labeling"
|
||||
valid, error = LabelStudioConfigValidator._validate_control(
|
||||
control, object_names, strict
|
||||
)
|
||||
if not valid and strict:
|
||||
return False, f"Control {control.tag}: {error}"
|
||||
|
||||
|
||||
return True, None
|
||||
|
||||
except ET.ParseError as e:
|
||||
@@ -59,7 +101,11 @@ class LabelStudioConfigValidator:
|
||||
return False, f"Validation error: {str(e)}"
|
||||
|
||||
@staticmethod
|
||||
def _validate_control(control: ET.Element) -> Tuple[bool, Optional[str]]:
|
||||
def _validate_control(
|
||||
control: ET.Element,
|
||||
object_names: Set[str],
|
||||
strict: bool
|
||||
) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
验证单个控件元素
|
||||
|
||||
@@ -72,26 +118,37 @@ class LabelStudioConfigValidator:
|
||||
config = LabelStudioConfigValidator._get_config()
|
||||
|
||||
# 检查必需属性
|
||||
if 'name' not in control.attrib:
|
||||
return False, "Missing 'name' attribute"
|
||||
|
||||
if 'toName' not in control.attrib:
|
||||
return False, "Missing 'toName' attribute"
|
||||
required_attrs = LabelStudioConfigValidator._get_required_attrs(
|
||||
control.tag, is_control=True
|
||||
)
|
||||
for attr in required_attrs:
|
||||
if not control.attrib.get(attr):
|
||||
return (False, f"Missing '{attr}' attribute") if strict else (True, None)
|
||||
|
||||
# 校验 toName 指向对象
|
||||
if strict and control.attrib.get("toName"):
|
||||
to_names = [
|
||||
name.strip()
|
||||
for name in control.attrib.get("toName", "").split(",")
|
||||
if name.strip()
|
||||
]
|
||||
invalid = [name for name in to_names if name not in object_names]
|
||||
if invalid:
|
||||
return False, f"toName references unknown object(s): {', '.join(invalid)}"
|
||||
|
||||
# 检查控件是否需要子元素
|
||||
if config.requires_children(control.tag):
|
||||
child_tag = config.get_child_tag(control.tag)
|
||||
if not child_tag:
|
||||
return False, f"Configuration error: no child_tag defined for {control.tag}"
|
||||
|
||||
return (False, f"Configuration error: no child_tag defined for {control.tag}") if strict else (True, None)
|
||||
|
||||
children = control.findall(child_tag)
|
||||
if not children:
|
||||
return False, f"{control.tag} must have at least one <{child_tag}> child"
|
||||
|
||||
# 检查每个子元素是否有value
|
||||
return (False, f"{control.tag} must have at least one <{child_tag}> child") if strict else (True, None)
|
||||
|
||||
for child in children:
|
||||
if 'value' not in child.attrib:
|
||||
return False, f"{child_tag} missing 'value' attribute"
|
||||
if "value" not in child.attrib or not child.attrib.get("value"):
|
||||
return (False, f"{child_tag} missing 'value' attribute") if strict else (True, None)
|
||||
|
||||
return True, None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user