diff --git a/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx b/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx index ac57e7b..4498dee 100644 --- a/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx +++ b/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx @@ -90,9 +90,11 @@ type SwitchDecision = "save" | "discard" | "cancel"; const LSF_IFRAME_SRC = "/lsf/lsf.html"; const TASK_PAGE_START = 0; const TASK_PAGE_SIZE = 200; -const NO_ANNOTATION_LABEL = "无标注/不适用"; +const NO_ANNOTATION_LABEL = "无标注"; +const NOT_APPLICABLE_LABEL = "不适用"; const NO_ANNOTATION_CONFIRM_TITLE = "没有标注任何内容"; const NO_ANNOTATION_CONFIRM_OK_TEXT = "设为无标注并保存"; +const NOT_APPLICABLE_CONFIRM_TEXT = "设为不适用并保存"; const NO_ANNOTATION_CONFIRM_CANCEL_TEXT = "继续标注"; type NormalizedTaskList = { @@ -140,6 +142,9 @@ const resolveTaskStatusMeta = (item: EditorTaskListItem) => { if (item.annotationStatus === AnnotationResultStatus.NO_ANNOTATION) { return { text: NO_ANNOTATION_LABEL, type: "warning" as const }; } + if (item.annotationStatus === AnnotationResultStatus.NOT_APPLICABLE) { + return { text: NOT_APPLICABLE_LABEL, type: "warning" as const }; + } return { text: "已标注", type: "success" as const }; }; @@ -271,26 +276,32 @@ export default function LabelStudioTextEditor() { win.postMessage({ type, payload }, origin); }, [origin]); - const confirmNoAnnotation = useCallback(() => { - return new Promise((resolve) => { + const confirmEmptyAnnotationStatus = useCallback(() => { + return new Promise((resolve) => { let resolved = false; - const settle = (value: boolean) => { + let modalInstance: { destroy: () => void } | null = null; + const settle = (value: AnnotationResultStatus | null) => { if (resolved) return; resolved = true; resolve(value); + if (modalInstance) modalInstance.destroy(); }; - modal.confirm({ + const handleNotApplicable = () => settle(AnnotationResultStatus.NOT_APPLICABLE); + modalInstance = modal.confirm({ title: NO_ANNOTATION_CONFIRM_TITLE, content: (
当前未发现任何标注内容。 - 如确认为无标注/不适用,可继续保存。 + 如确认为无标注或不适用,可继续保存。 +
), okText: NO_ANNOTATION_CONFIRM_OK_TEXT, cancelText: NO_ANNOTATION_CONFIRM_CANCEL_TEXT, - onOk: () => settle(true), - onCancel: () => settle(false), + onOk: () => settle(AnnotationResultStatus.NO_ANNOTATION), + onCancel: () => settle(null), }); }); }, [modal]); @@ -588,15 +599,17 @@ export default function LabelStudioTextEditor() { : undefined; const annotationRecord = annotation as Record; + const currentTask = tasks.find((item) => item.fileId === String(fileId)); + const currentStatus = currentTask?.annotationStatus; + const hasExistingAnnotation = !!currentTask?.hasAnnotation; let resolvedStatus: AnnotationResultStatus; if (isAnnotationResultEmpty(annotationRecord)) { - const currentStatus = tasks.find((item) => item.fileId === String(fileId))?.annotationStatus; - if (currentStatus === AnnotationResultStatus.NO_ANNOTATION) { - resolvedStatus = AnnotationResultStatus.NO_ANNOTATION; + if (currentStatus === AnnotationResultStatus.ANNOTATED || (hasExistingAnnotation && !currentStatus)) { + resolvedStatus = AnnotationResultStatus.ANNOTATED; } else { - const confirmed = await confirmNoAnnotation(); - if (!confirmed) return false; - resolvedStatus = AnnotationResultStatus.NO_ANNOTATION; + const selectedStatus = await confirmEmptyAnnotationStatus(); + if (!selectedStatus) return false; + resolvedStatus = selectedStatus; } } else { resolvedStatus = AnnotationResultStatus.ANNOTATED; @@ -651,7 +664,7 @@ export default function LabelStudioTextEditor() { } }, [ advanceAfterSave, - confirmNoAnnotation, + confirmEmptyAnnotationStatus, currentSegmentIndex, message, projectId, diff --git a/frontend/src/pages/DataAnnotation/annotation.model.ts b/frontend/src/pages/DataAnnotation/annotation.model.ts index 3d53c7c..a63f30c 100644 --- a/frontend/src/pages/DataAnnotation/annotation.model.ts +++ b/frontend/src/pages/DataAnnotation/annotation.model.ts @@ -11,6 +11,7 @@ export enum AnnotationTaskStatus { export enum AnnotationResultStatus { ANNOTATED = "ANNOTATED", NO_ANNOTATION = "NO_ANNOTATION", + NOT_APPLICABLE = "NOT_APPLICABLE", } export interface AnnotationTask { diff --git a/runtime/datamate-python/app/db/models/annotation_management.py b/runtime/datamate-python/app/db/models/annotation_management.py index 6454306..b5f2444 100644 --- a/runtime/datamate-python/app/db/models/annotation_management.py +++ b/runtime/datamate-python/app/db/models/annotation_management.py @@ -8,7 +8,12 @@ from app.db.session import Base ANNOTATION_STATUS_ANNOTATED = "ANNOTATED" ANNOTATION_STATUS_NO_ANNOTATION = "NO_ANNOTATION" -ANNOTATION_STATUS_VALUES = {ANNOTATION_STATUS_ANNOTATED, ANNOTATION_STATUS_NO_ANNOTATION} +ANNOTATION_STATUS_NOT_APPLICABLE = "NOT_APPLICABLE" +ANNOTATION_STATUS_VALUES = { + ANNOTATION_STATUS_ANNOTATED, + ANNOTATION_STATUS_NO_ANNOTATION, + ANNOTATION_STATUS_NOT_APPLICABLE, +} class AnnotationTemplate(Base): """标注配置模板模型""" @@ -96,7 +101,7 @@ class AnnotationResult(Base): String(32), nullable=False, default=ANNOTATION_STATUS_ANNOTATED, - comment="标注状态: ANNOTATED/NO_ANNOTATION", + comment="标注状态: ANNOTATED/NO_ANNOTATION/NOT_APPLICABLE", ) created_at = Column(TIMESTAMP, server_default=func.current_timestamp(), comment="创建时间") updated_at = Column(TIMESTAMP, server_default=func.current_timestamp(), onupdate=func.current_timestamp(), comment="更新时间") diff --git a/runtime/datamate-python/app/module/annotation/schema/editor.py b/runtime/datamate-python/app/module/annotation/schema/editor.py index 127c6ee..36711b3 100644 --- a/runtime/datamate-python/app/module/annotation/schema/editor.py +++ b/runtime/datamate-python/app/module/annotation/schema/editor.py @@ -17,6 +17,7 @@ from pydantic import BaseModel, Field, ConfigDict from app.db.models.annotation_management import ( ANNOTATION_STATUS_ANNOTATED, ANNOTATION_STATUS_NO_ANNOTATION, + ANNOTATION_STATUS_NOT_APPLICABLE, ) @@ -25,6 +26,7 @@ class AnnotationStatus(str, Enum): ANNOTATED = ANNOTATION_STATUS_ANNOTATED NO_ANNOTATION = ANNOTATION_STATUS_NO_ANNOTATION + NOT_APPLICABLE = ANNOTATION_STATUS_NOT_APPLICABLE class EditorProjectInfo(BaseModel): @@ -110,7 +112,7 @@ class UpsertAnnotationRequest(BaseModel): annotation_status: Optional[AnnotationStatus] = Field( None, alias="annotationStatus", - description="标注状态(无标注/不适用时传 NO_ANNOTATION)", + description="标注状态(无标注传 NO_ANNOTATION,不适用传 NOT_APPLICABLE)", ) expected_updated_at: Optional[datetime] = Field( None, diff --git a/runtime/datamate-python/app/module/annotation/service/editor.py b/runtime/datamate-python/app/module/annotation/service/editor.py index 51162dd..a3aa466 100644 --- a/runtime/datamate-python/app/module/annotation/service/editor.py +++ b/runtime/datamate-python/app/module/annotation/service/editor.py @@ -27,6 +27,7 @@ from app.db.models import AnnotationResult, Dataset, DatasetFiles, LabelingProje from app.db.models.annotation_management import ( ANNOTATION_STATUS_ANNOTATED, ANNOTATION_STATUS_NO_ANNOTATION, + ANNOTATION_STATUS_NOT_APPLICABLE, ANNOTATION_STATUS_VALUES, ) from app.module.annotation.config import LabelStudioTagConfig @@ -908,6 +909,8 @@ class AnnotationEditorService: else: if requested_status == ANNOTATION_STATUS_NO_ANNOTATION: final_status = ANNOTATION_STATUS_NO_ANNOTATION + elif requested_status == ANNOTATION_STATUS_NOT_APPLICABLE: + final_status = ANNOTATION_STATUS_NOT_APPLICABLE else: raise HTTPException(status_code=400, detail="未发现标注内容,请确认无标注/不适用后再保存") diff --git a/scripts/db/data-annotation-init.sql b/scripts/db/data-annotation-init.sql index 0c876a3..f5c3f03 100644 --- a/scripts/db/data-annotation-init.sql +++ b/scripts/db/data-annotation-init.sql @@ -65,7 +65,7 @@ CREATE TABLE IF NOT EXISTS t_dm_annotation_results ( project_id VARCHAR(36) NOT NULL COMMENT '标注项目ID', file_id VARCHAR(36) NOT NULL COMMENT '文件ID', annotation JSON NOT NULL COMMENT 'Label Studio annotation 原始JSON', - annotation_status VARCHAR(32) NOT NULL DEFAULT 'ANNOTATED' COMMENT '标注状态: ANNOTATED/NO_ANNOTATION', + annotation_status VARCHAR(32) NOT NULL DEFAULT 'ANNOTATED' COMMENT '标注状态: ANNOTATED/NO_ANNOTATION/NOT_APPLICABLE', created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', UNIQUE KEY uk_project_file (project_id, file_id),