feat(annotation): 添加标注状态管理功能

- 引入 AnnotationResultStatus 枚举类型区分已标注和无标注状态
- 在前端组件中实现空标注检测和确认对话框逻辑
- 添加数据库表字段 annotation_status 存储标注状态
- 扩展后端服务验证和处理标注状态逻辑
- 更新 API 接口支持标注状态参数传递
- 改进任务列表显示逻辑以反映不同标注状态
- 实现分段模式下的标注结果检查机制
This commit is contained in:
2026-01-31 13:23:38 +08:00
parent 52a2a73a8e
commit f4fc574687
6 changed files with 194 additions and 44 deletions

View File

@@ -24,6 +24,11 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import settings
from app.core.logging import get_logger
from app.db.models import AnnotationResult, Dataset, DatasetFiles, LabelingProject, LabelingProjectFile
from app.db.models.annotation_management import (
ANNOTATION_STATUS_ANNOTATED,
ANNOTATION_STATUS_NO_ANNOTATION,
ANNOTATION_STATUS_VALUES,
)
from app.module.annotation.config import LabelStudioTagConfig
from app.module.annotation.schema.editor import (
EditorProjectInfo,
@@ -348,6 +353,24 @@ class AnnotationEditorService:
return ET.tostring(root, encoding="unicode")
@staticmethod
def _has_annotation_result(payload: Optional[Dict[str, Any]]) -> bool:
if not payload or not isinstance(payload, dict):
return False
if payload.get("segmented"):
segments = payload.get("segments", {})
if not isinstance(segments, dict):
return False
for segment in segments.values():
if not isinstance(segment, dict):
continue
result = segment.get("result")
if isinstance(result, list) and len(result) > 0:
return True
return False
result = payload.get("result")
return isinstance(result, list) and len(result) > 0
@classmethod
def _build_source_document_filter(cls):
file_type_lower = func.lower(DatasetFiles.file_type)
@@ -447,7 +470,12 @@ class AnnotationEditorService:
else_=0,
)
files_result = await self.db.execute(
select(DatasetFiles, AnnotationResult.id, AnnotationResult.updated_at)
select(
DatasetFiles,
AnnotationResult.id,
AnnotationResult.updated_at,
AnnotationResult.annotation_status,
)
.join(LabelingProjectFile, LabelingProjectFile.file_id == DatasetFiles.id)
.outerjoin(
AnnotationResult,
@@ -462,7 +490,7 @@ class AnnotationEditorService:
rows = files_result.all()
items: List[EditorTaskListItem] = []
for file_record, annotation_id, annotation_updated_at in rows:
for file_record, annotation_id, annotation_updated_at, annotation_status in rows:
fid = str(file_record.id) # type: ignore[arg-type]
items.append(
EditorTaskListItem(
@@ -471,6 +499,7 @@ class AnnotationEditorService:
fileType=getattr(file_record, "file_type", None),
hasAnnotation=annotation_id is not None,
annotationUpdatedAt=annotation_updated_at,
annotationStatus=annotation_status,
)
)
@@ -869,12 +898,26 @@ class AnnotationEditorService:
annotation_payload["id"] = self._make_ls_annotation_id(project_id, file_id)
final_payload = annotation_payload
requested_status = request.annotation_status
if requested_status is not None and requested_status not in ANNOTATION_STATUS_VALUES:
raise HTTPException(status_code=400, detail="annotationStatus 不合法")
has_result = self._has_annotation_result(final_payload)
if has_result:
final_status = ANNOTATION_STATUS_ANNOTATED
else:
if requested_status == ANNOTATION_STATUS_NO_ANNOTATION:
final_status = ANNOTATION_STATUS_NO_ANNOTATION
else:
raise HTTPException(status_code=400, detail="未发现标注内容,请确认无标注/不适用后再保存")
if existing:
if request.expected_updated_at and existing.updated_at:
if existing.updated_at != request.expected_updated_at.replace(tzinfo=None):
raise HTTPException(status_code=409, detail="标注已被更新,请刷新后重试")
existing.annotation = final_payload # type: ignore[assignment]
existing.annotation_status = final_status # type: ignore[assignment]
existing.updated_at = now # type: ignore[assignment]
await self.db.commit()
await self.db.refresh(existing)
@@ -892,6 +935,7 @@ class AnnotationEditorService:
project_id=project_id,
file_id=file_id,
annotation=final_payload,
annotation_status=final_status,
created_at=now,
updated_at=now,
)