feat: Add labeling template (#72)

* feat: Enhance annotation module with template management and validation

- Added DatasetMappingCreateRequest and DatasetMappingUpdateRequest schemas to handle dataset mapping requests with camelCase and snake_case support.
- Introduced Annotation Template schemas including CreateAnnotationTemplateRequest, UpdateAnnotationTemplateRequest, and AnnotationTemplateResponse for managing annotation templates.
- Implemented AnnotationTemplateService for creating, updating, retrieving, and deleting annotation templates, including validation of configurations and XML generation.
- Added utility class LabelStudioConfigValidator for validating Label Studio configurations and XML formats.
- Updated database schema for annotation templates and labeling projects to include new fields and constraints.
- Seeded initial annotation templates for various use cases including image classification, object detection, and text classification.

* feat: Enhance TemplateForm with improved validation and dynamic field rendering; update LabelStudio config validation for camelCase support

* feat: Update docker-compose.yml to mark datamate dataset volume and network as external
This commit is contained in:
Jason Wang
2025-11-11 09:14:14 +08:00
committed by GitHub
parent 451d3c8207
commit c5ccc56cca
24 changed files with 2794 additions and 253 deletions

View File

@@ -3,6 +3,7 @@ from fastapi import APIRouter
from .about import router as about_router
from .project import router as project_router
from .task import router as task_router
from .template import router as template_router
router = APIRouter(
prefix="/annotation",
@@ -11,4 +12,5 @@ router = APIRouter(
router.include_router(about_router)
router.include_router(project_router)
router.include_router(task_router)
router.include_router(task_router)
router.include_router(template_router)

View File

@@ -1,5 +1,6 @@
from typing import Optional
import math
import uuid
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
@@ -14,6 +15,7 @@ from app.core.config import settings
from ..client import LabelStudioClient
from ..service.mapping import DatasetMappingService
from ..service.sync import SyncService
from ..service.template import AnnotationTemplateService
from ..schema import (
DatasetMappingCreateRequest,
DatasetMappingCreateResponse,
@@ -39,6 +41,8 @@ async def create_mapping(
在数据库中记录这一关联关系,返回Label Studio数据集的ID
注意:一个数据集可以创建多个标注项目
支持通过 template_id 指定标注模板,如果提供了模板ID,则使用模板的配置
"""
try:
dm_client = DatasetManagementService(db)
@@ -46,6 +50,7 @@ async def create_mapping(
token=settings.label_studio_user_token)
mapping_service = DatasetMappingService(db)
sync_service = SyncService(dm_client, ls_client, mapping_service)
template_service = AnnotationTemplateService()
logger.info(f"Create dataset mapping request: {request.dataset_id}")
@@ -65,10 +70,24 @@ async def create_mapping(
dataset_info.description or \
f"Imported from DM dataset {dataset_info.name} ({dataset_info.id})"
# 如果提供了模板ID,获取模板配置
label_config = None
if request.template_id:
logger.info(f"Using template: {request.template_id}")
template = await template_service.get_template(db, request.template_id)
if not template:
raise HTTPException(
status_code=404,
detail=f"Template not found: {request.template_id}"
)
label_config = template.label_config
logger.debug(f"Template label config loaded for template: {template.name}")
# 在Label Studio中创建项目
project_data = await ls_client.create_project(
title=project_name,
description=project_description,
label_config=label_config # 传递模板配置
)
if not project_data:
@@ -96,9 +115,11 @@ async def create_mapping(
logger.info(f"Local storage configured for project {project_id}: {local_storage_path}")
labeling_project = LabelingProject(
id=str(uuid.uuid4()), # Generate UUID here
dataset_id=request.dataset_id,
labeling_project_id=str(project_id),
name=project_name,
template_id=request.template_id, # Save template_id to database
)
# 创建映射关系,包含项目名称(先持久化映射以获得 mapping.id)

View File

@@ -0,0 +1,137 @@
"""
Annotation Template API Endpoints
"""
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_db
from app.module.shared.schema import StandardResponse
from app.module.annotation.schema.template import (
CreateAnnotationTemplateRequest,
UpdateAnnotationTemplateRequest,
AnnotationTemplateResponse,
AnnotationTemplateListResponse
)
from app.module.annotation.service.template import AnnotationTemplateService
router = APIRouter(prefix="/templates", tags=["Annotation Template"])
template_service = AnnotationTemplateService()
@router.post(
"",
response_model=StandardResponse[AnnotationTemplateResponse],
summary="创建标注模板"
)
async def create_template(
request: CreateAnnotationTemplateRequest,
db: AsyncSession = Depends(get_db)
):
"""
创建新的标注模板
- **name**: 模板名称(必填,最多100字符)
- **description**: 模板描述(可选,最多500字符)
- **dataType**: 数据类型(必填)
- **labelingType**: 标注类型(必填)
- **configuration**: 标注配置(必填,包含labels和objects)
- **style**: 样式配置(默认horizontal)
- **category**: 模板分类(默认custom)
"""
template = await template_service.create_template(db, request)
return StandardResponse(code=200, message="success", data=template)
@router.get(
"/{template_id}",
response_model=StandardResponse[AnnotationTemplateResponse],
summary="获取模板详情"
)
async def get_template(
template_id: str,
db: AsyncSession = Depends(get_db)
):
"""
根据ID获取模板详情
"""
template = await template_service.get_template(db, template_id)
if not template:
raise HTTPException(status_code=404, detail="Template not found")
return StandardResponse(code=200, message="success", data=template)
@router.get(
"",
response_model=StandardResponse[AnnotationTemplateListResponse],
summary="获取模板列表"
)
async def list_templates(
page: int = Query(1, ge=1, description="页码"),
size: int = Query(10, ge=1, le=100, description="每页大小"),
category: Optional[str] = Query(None, description="分类筛选"),
dataType: Optional[str] = Query(None, alias="dataType", description="数据类型筛选"),
labelingType: Optional[str] = Query(None, alias="labelingType", description="标注类型筛选"),
builtIn: Optional[bool] = Query(None, alias="builtIn", description="是否内置模板"),
db: AsyncSession = Depends(get_db)
):
"""
获取模板列表,支持分页和筛选
- **page**: 页码(从1开始)
- **size**: 每页大小(1-100)
- **category**: 模板分类筛选
- **dataType**: 数据类型筛选
- **labelingType**: 标注类型筛选
- **builtIn**: 是否只显示内置模板
"""
templates = await template_service.list_templates(
db=db,
page=page,
size=size,
category=category,
data_type=dataType,
labeling_type=labelingType,
built_in=builtIn
)
return StandardResponse(code=200, message="success", data=templates)
@router.put(
"/{template_id}",
response_model=StandardResponse[AnnotationTemplateResponse],
summary="更新模板"
)
async def update_template(
template_id: str,
request: UpdateAnnotationTemplateRequest,
db: AsyncSession = Depends(get_db)
):
"""
更新模板信息
所有字段都是可选的,只更新提供的字段
"""
template = await template_service.update_template(db, template_id, request)
if not template:
raise HTTPException(status_code=404, detail="Template not found")
return StandardResponse(code=200, message="success", data=template)
@router.delete(
"/{template_id}",
response_model=StandardResponse[bool],
summary="删除模板"
)
async def delete_template(
template_id: str,
db: AsyncSession = Depends(get_db)
):
"""
删除模板(软删除)
"""
success = await template_service.delete_template(db, template_id)
if not success:
raise HTTPException(status_code=404, detail="Template not found")
return StandardResponse(code=200, message="success", data=True)