You've already forked DataMate
feat(annotation): 添加不适用标注状态支持
- 在 AnnotationResultStatus 枚举中新增 NOT_APPLICABLE 状态 - 将无标注/不适用合并为两个独立的状态选项 - 更新前端标签显示逻辑以支持新的状态类型 - 修改确认对话框允许选择不适用状态 - 在后端数据库模型中添加 NOT_APPLICABLE 状态值 - 更新 API schema 描述以反映新的状态选项 - 调整标注状态判断和保存逻辑以处理三种状态 - 更新数据库表结构注释包含新状态类型
This commit is contained in:
@@ -90,9 +90,11 @@ type SwitchDecision = "save" | "discard" | "cancel";
|
|||||||
const LSF_IFRAME_SRC = "/lsf/lsf.html";
|
const LSF_IFRAME_SRC = "/lsf/lsf.html";
|
||||||
const TASK_PAGE_START = 0;
|
const TASK_PAGE_START = 0;
|
||||||
const TASK_PAGE_SIZE = 200;
|
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_TITLE = "没有标注任何内容";
|
||||||
const NO_ANNOTATION_CONFIRM_OK_TEXT = "设为无标注并保存";
|
const NO_ANNOTATION_CONFIRM_OK_TEXT = "设为无标注并保存";
|
||||||
|
const NOT_APPLICABLE_CONFIRM_TEXT = "设为不适用并保存";
|
||||||
const NO_ANNOTATION_CONFIRM_CANCEL_TEXT = "继续标注";
|
const NO_ANNOTATION_CONFIRM_CANCEL_TEXT = "继续标注";
|
||||||
|
|
||||||
type NormalizedTaskList = {
|
type NormalizedTaskList = {
|
||||||
@@ -140,6 +142,9 @@ const resolveTaskStatusMeta = (item: EditorTaskListItem) => {
|
|||||||
if (item.annotationStatus === AnnotationResultStatus.NO_ANNOTATION) {
|
if (item.annotationStatus === AnnotationResultStatus.NO_ANNOTATION) {
|
||||||
return { text: NO_ANNOTATION_LABEL, type: "warning" as const };
|
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 };
|
return { text: "已标注", type: "success" as const };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -271,26 +276,32 @@ export default function LabelStudioTextEditor() {
|
|||||||
win.postMessage({ type, payload }, origin);
|
win.postMessage({ type, payload }, origin);
|
||||||
}, [origin]);
|
}, [origin]);
|
||||||
|
|
||||||
const confirmNoAnnotation = useCallback(() => {
|
const confirmEmptyAnnotationStatus = useCallback(() => {
|
||||||
return new Promise<boolean>((resolve) => {
|
return new Promise<AnnotationResultStatus | null>((resolve) => {
|
||||||
let resolved = false;
|
let resolved = false;
|
||||||
const settle = (value: boolean) => {
|
let modalInstance: { destroy: () => void } | null = null;
|
||||||
|
const settle = (value: AnnotationResultStatus | null) => {
|
||||||
if (resolved) return;
|
if (resolved) return;
|
||||||
resolved = true;
|
resolved = true;
|
||||||
resolve(value);
|
resolve(value);
|
||||||
|
if (modalInstance) modalInstance.destroy();
|
||||||
};
|
};
|
||||||
modal.confirm({
|
const handleNotApplicable = () => settle(AnnotationResultStatus.NOT_APPLICABLE);
|
||||||
|
modalInstance = modal.confirm({
|
||||||
title: NO_ANNOTATION_CONFIRM_TITLE,
|
title: NO_ANNOTATION_CONFIRM_TITLE,
|
||||||
content: (
|
content: (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Typography.Text>当前未发现任何标注内容。</Typography.Text>
|
<Typography.Text>当前未发现任何标注内容。</Typography.Text>
|
||||||
<Typography.Text type="secondary">如确认为无标注/不适用,可继续保存。</Typography.Text>
|
<Typography.Text type="secondary">如确认为无标注或不适用,可继续保存。</Typography.Text>
|
||||||
|
<Button type="link" style={{ padding: 0, height: "auto" }} onClick={handleNotApplicable}>
|
||||||
|
{NOT_APPLICABLE_CONFIRM_TEXT}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
okText: NO_ANNOTATION_CONFIRM_OK_TEXT,
|
okText: NO_ANNOTATION_CONFIRM_OK_TEXT,
|
||||||
cancelText: NO_ANNOTATION_CONFIRM_CANCEL_TEXT,
|
cancelText: NO_ANNOTATION_CONFIRM_CANCEL_TEXT,
|
||||||
onOk: () => settle(true),
|
onOk: () => settle(AnnotationResultStatus.NO_ANNOTATION),
|
||||||
onCancel: () => settle(false),
|
onCancel: () => settle(null),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, [modal]);
|
}, [modal]);
|
||||||
@@ -588,15 +599,17 @@ export default function LabelStudioTextEditor() {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const annotationRecord = annotation as Record<string, unknown>;
|
const annotationRecord = annotation as Record<string, unknown>;
|
||||||
|
const currentTask = tasks.find((item) => item.fileId === String(fileId));
|
||||||
|
const currentStatus = currentTask?.annotationStatus;
|
||||||
|
const hasExistingAnnotation = !!currentTask?.hasAnnotation;
|
||||||
let resolvedStatus: AnnotationResultStatus;
|
let resolvedStatus: AnnotationResultStatus;
|
||||||
if (isAnnotationResultEmpty(annotationRecord)) {
|
if (isAnnotationResultEmpty(annotationRecord)) {
|
||||||
const currentStatus = tasks.find((item) => item.fileId === String(fileId))?.annotationStatus;
|
if (currentStatus === AnnotationResultStatus.ANNOTATED || (hasExistingAnnotation && !currentStatus)) {
|
||||||
if (currentStatus === AnnotationResultStatus.NO_ANNOTATION) {
|
resolvedStatus = AnnotationResultStatus.ANNOTATED;
|
||||||
resolvedStatus = AnnotationResultStatus.NO_ANNOTATION;
|
|
||||||
} else {
|
} else {
|
||||||
const confirmed = await confirmNoAnnotation();
|
const selectedStatus = await confirmEmptyAnnotationStatus();
|
||||||
if (!confirmed) return false;
|
if (!selectedStatus) return false;
|
||||||
resolvedStatus = AnnotationResultStatus.NO_ANNOTATION;
|
resolvedStatus = selectedStatus;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
resolvedStatus = AnnotationResultStatus.ANNOTATED;
|
resolvedStatus = AnnotationResultStatus.ANNOTATED;
|
||||||
@@ -651,7 +664,7 @@ export default function LabelStudioTextEditor() {
|
|||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
advanceAfterSave,
|
advanceAfterSave,
|
||||||
confirmNoAnnotation,
|
confirmEmptyAnnotationStatus,
|
||||||
currentSegmentIndex,
|
currentSegmentIndex,
|
||||||
message,
|
message,
|
||||||
projectId,
|
projectId,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export enum AnnotationTaskStatus {
|
|||||||
export enum AnnotationResultStatus {
|
export enum AnnotationResultStatus {
|
||||||
ANNOTATED = "ANNOTATED",
|
ANNOTATED = "ANNOTATED",
|
||||||
NO_ANNOTATION = "NO_ANNOTATION",
|
NO_ANNOTATION = "NO_ANNOTATION",
|
||||||
|
NOT_APPLICABLE = "NOT_APPLICABLE",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AnnotationTask {
|
export interface AnnotationTask {
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ from app.db.session import Base
|
|||||||
|
|
||||||
ANNOTATION_STATUS_ANNOTATED = "ANNOTATED"
|
ANNOTATION_STATUS_ANNOTATED = "ANNOTATED"
|
||||||
ANNOTATION_STATUS_NO_ANNOTATION = "NO_ANNOTATION"
|
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):
|
class AnnotationTemplate(Base):
|
||||||
"""标注配置模板模型"""
|
"""标注配置模板模型"""
|
||||||
@@ -96,7 +101,7 @@ class AnnotationResult(Base):
|
|||||||
String(32),
|
String(32),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
default=ANNOTATION_STATUS_ANNOTATED,
|
default=ANNOTATION_STATUS_ANNOTATED,
|
||||||
comment="标注状态: ANNOTATED/NO_ANNOTATION",
|
comment="标注状态: ANNOTATED/NO_ANNOTATION/NOT_APPLICABLE",
|
||||||
)
|
)
|
||||||
created_at = Column(TIMESTAMP, server_default=func.current_timestamp(), comment="创建时间")
|
created_at = Column(TIMESTAMP, server_default=func.current_timestamp(), comment="创建时间")
|
||||||
updated_at = Column(TIMESTAMP, server_default=func.current_timestamp(), onupdate=func.current_timestamp(), comment="更新时间")
|
updated_at = Column(TIMESTAMP, server_default=func.current_timestamp(), onupdate=func.current_timestamp(), comment="更新时间")
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from pydantic import BaseModel, Field, ConfigDict
|
|||||||
from app.db.models.annotation_management import (
|
from app.db.models.annotation_management import (
|
||||||
ANNOTATION_STATUS_ANNOTATED,
|
ANNOTATION_STATUS_ANNOTATED,
|
||||||
ANNOTATION_STATUS_NO_ANNOTATION,
|
ANNOTATION_STATUS_NO_ANNOTATION,
|
||||||
|
ANNOTATION_STATUS_NOT_APPLICABLE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ class AnnotationStatus(str, Enum):
|
|||||||
|
|
||||||
ANNOTATED = ANNOTATION_STATUS_ANNOTATED
|
ANNOTATED = ANNOTATION_STATUS_ANNOTATED
|
||||||
NO_ANNOTATION = ANNOTATION_STATUS_NO_ANNOTATION
|
NO_ANNOTATION = ANNOTATION_STATUS_NO_ANNOTATION
|
||||||
|
NOT_APPLICABLE = ANNOTATION_STATUS_NOT_APPLICABLE
|
||||||
|
|
||||||
|
|
||||||
class EditorProjectInfo(BaseModel):
|
class EditorProjectInfo(BaseModel):
|
||||||
@@ -110,7 +112,7 @@ class UpsertAnnotationRequest(BaseModel):
|
|||||||
annotation_status: Optional[AnnotationStatus] = Field(
|
annotation_status: Optional[AnnotationStatus] = Field(
|
||||||
None,
|
None,
|
||||||
alias="annotationStatus",
|
alias="annotationStatus",
|
||||||
description="标注状态(无标注/不适用时传 NO_ANNOTATION)",
|
description="标注状态(无标注传 NO_ANNOTATION,不适用传 NOT_APPLICABLE)",
|
||||||
)
|
)
|
||||||
expected_updated_at: Optional[datetime] = Field(
|
expected_updated_at: Optional[datetime] = Field(
|
||||||
None,
|
None,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ from app.db.models import AnnotationResult, Dataset, DatasetFiles, LabelingProje
|
|||||||
from app.db.models.annotation_management import (
|
from app.db.models.annotation_management import (
|
||||||
ANNOTATION_STATUS_ANNOTATED,
|
ANNOTATION_STATUS_ANNOTATED,
|
||||||
ANNOTATION_STATUS_NO_ANNOTATION,
|
ANNOTATION_STATUS_NO_ANNOTATION,
|
||||||
|
ANNOTATION_STATUS_NOT_APPLICABLE,
|
||||||
ANNOTATION_STATUS_VALUES,
|
ANNOTATION_STATUS_VALUES,
|
||||||
)
|
)
|
||||||
from app.module.annotation.config import LabelStudioTagConfig
|
from app.module.annotation.config import LabelStudioTagConfig
|
||||||
@@ -908,6 +909,8 @@ class AnnotationEditorService:
|
|||||||
else:
|
else:
|
||||||
if requested_status == ANNOTATION_STATUS_NO_ANNOTATION:
|
if requested_status == ANNOTATION_STATUS_NO_ANNOTATION:
|
||||||
final_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:
|
else:
|
||||||
raise HTTPException(status_code=400, detail="未发现标注内容,请确认无标注/不适用后再保存")
|
raise HTTPException(status_code=400, detail="未发现标注内容,请确认无标注/不适用后再保存")
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ CREATE TABLE IF NOT EXISTS t_dm_annotation_results (
|
|||||||
project_id VARCHAR(36) NOT NULL COMMENT '标注项目ID',
|
project_id VARCHAR(36) NOT NULL COMMENT '标注项目ID',
|
||||||
file_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 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 '创建时间',
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
UNIQUE KEY uk_project_file (project_id, file_id),
|
UNIQUE KEY uk_project_file (project_id, file_id),
|
||||||
|
|||||||
Reference in New Issue
Block a user