You've already forked DataMate
Compare commits
2 Commits
52a2a73a8e
...
f2403f00ce
| Author | SHA1 | Date | |
|---|---|---|---|
| f2403f00ce | |||
| f4fc574687 |
@@ -9,6 +9,7 @@ import {
|
|||||||
listEditorTasksUsingGet,
|
listEditorTasksUsingGet,
|
||||||
upsertEditorAnnotationUsingPut,
|
upsertEditorAnnotationUsingPut,
|
||||||
} from "../annotation.api";
|
} from "../annotation.api";
|
||||||
|
import { AnnotationResultStatus } from "../annotation.model";
|
||||||
|
|
||||||
type EditorProjectInfo = {
|
type EditorProjectInfo = {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@@ -26,6 +27,7 @@ type EditorTaskListItem = {
|
|||||||
fileType?: string | null;
|
fileType?: string | null;
|
||||||
hasAnnotation: boolean;
|
hasAnnotation: boolean;
|
||||||
annotationUpdatedAt?: string | null;
|
annotationUpdatedAt?: string | null;
|
||||||
|
annotationStatus?: AnnotationResultStatus | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type LsfMessage = {
|
type LsfMessage = {
|
||||||
@@ -88,6 +90,12 @@ 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 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 = {
|
type NormalizedTaskList = {
|
||||||
items: EditorTaskListItem[];
|
items: EditorTaskListItem[];
|
||||||
@@ -119,6 +127,27 @@ const resolvePayloadMessage = (payload: unknown) => {
|
|||||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||||
!!value && typeof value === "object" && !Array.isArray(value);
|
!!value && typeof value === "object" && !Array.isArray(value);
|
||||||
|
|
||||||
|
const isAnnotationResultEmpty = (annotation?: Record<string, unknown>) => {
|
||||||
|
if (!annotation) return true;
|
||||||
|
if (!("result" in annotation)) return true;
|
||||||
|
const result = (annotation as { result?: unknown }).result;
|
||||||
|
if (!Array.isArray(result)) return false;
|
||||||
|
return result.length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveTaskStatusMeta = (item: EditorTaskListItem) => {
|
||||||
|
if (!item.hasAnnotation) {
|
||||||
|
return { text: "未标注", type: "secondary" as const };
|
||||||
|
}
|
||||||
|
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 };
|
||||||
|
};
|
||||||
|
|
||||||
const normalizeSnapshotValue = (value: unknown, seen: WeakSet<object>): unknown => {
|
const normalizeSnapshotValue = (value: unknown, seen: WeakSet<object>): unknown => {
|
||||||
if (!value || typeof value !== "object") return value;
|
if (!value || typeof value !== "object") return value;
|
||||||
const obj = value as object;
|
const obj = value as object;
|
||||||
@@ -247,6 +276,36 @@ export default function LabelStudioTextEditor() {
|
|||||||
win.postMessage({ type, payload }, origin);
|
win.postMessage({ type, payload }, origin);
|
||||||
}, [origin]);
|
}, [origin]);
|
||||||
|
|
||||||
|
const confirmEmptyAnnotationStatus = useCallback(() => {
|
||||||
|
return new Promise<AnnotationResultStatus | null>((resolve) => {
|
||||||
|
let resolved = false;
|
||||||
|
let modalInstance: { destroy: () => void } | null = null;
|
||||||
|
const settle = (value: AnnotationResultStatus | null) => {
|
||||||
|
if (resolved) return;
|
||||||
|
resolved = true;
|
||||||
|
resolve(value);
|
||||||
|
if (modalInstance) modalInstance.destroy();
|
||||||
|
};
|
||||||
|
const handleNotApplicable = () => settle(AnnotationResultStatus.NOT_APPLICABLE);
|
||||||
|
modalInstance = modal.confirm({
|
||||||
|
title: NO_ANNOTATION_CONFIRM_TITLE,
|
||||||
|
content: (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Typography.Text>当前未发现任何标注内容。</Typography.Text>
|
||||||
|
<Typography.Text type="secondary">如确认为无标注或不适用,可继续保存。</Typography.Text>
|
||||||
|
<Button type="link" style={{ padding: 0, height: "auto" }} onClick={handleNotApplicable}>
|
||||||
|
{NOT_APPLICABLE_CONFIRM_TEXT}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
okText: NO_ANNOTATION_CONFIRM_OK_TEXT,
|
||||||
|
cancelText: NO_ANNOTATION_CONFIRM_CANCEL_TEXT,
|
||||||
|
onOk: () => settle(AnnotationResultStatus.NO_ANNOTATION),
|
||||||
|
onCancel: () => settle(null),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [modal]);
|
||||||
|
|
||||||
const loadProject = useCallback(async () => {
|
const loadProject = useCallback(async () => {
|
||||||
setLoadingProject(true);
|
setLoadingProject(true);
|
||||||
try {
|
try {
|
||||||
@@ -539,11 +598,29 @@ export default function LabelStudioTextEditor() {
|
|||||||
? currentSegmentIndex
|
? currentSegmentIndex
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
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;
|
||||||
|
if (isAnnotationResultEmpty(annotationRecord)) {
|
||||||
|
if (currentStatus === AnnotationResultStatus.ANNOTATED || (hasExistingAnnotation && !currentStatus)) {
|
||||||
|
resolvedStatus = AnnotationResultStatus.ANNOTATED;
|
||||||
|
} else {
|
||||||
|
const selectedStatus = await confirmEmptyAnnotationStatus();
|
||||||
|
if (!selectedStatus) return false;
|
||||||
|
resolvedStatus = selectedStatus;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolvedStatus = AnnotationResultStatus.ANNOTATED;
|
||||||
|
}
|
||||||
|
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
const resp = (await upsertEditorAnnotationUsingPut(projectId, String(fileId), {
|
const resp = (await upsertEditorAnnotationUsingPut(projectId, String(fileId), {
|
||||||
annotation,
|
annotation,
|
||||||
segmentIndex,
|
segmentIndex,
|
||||||
|
annotationStatus: resolvedStatus,
|
||||||
})) as ApiResponse<UpsertAnnotationResponse>;
|
})) as ApiResponse<UpsertAnnotationResponse>;
|
||||||
const updatedAt = resp?.data?.updatedAt;
|
const updatedAt = resp?.data?.updatedAt;
|
||||||
message.success("标注已保存");
|
message.success("标注已保存");
|
||||||
@@ -553,6 +630,7 @@ export default function LabelStudioTextEditor() {
|
|||||||
? {
|
? {
|
||||||
...item,
|
...item,
|
||||||
hasAnnotation: true,
|
hasAnnotation: true,
|
||||||
|
annotationStatus: resolvedStatus,
|
||||||
annotationUpdatedAt: updatedAt || item.annotationUpdatedAt,
|
annotationUpdatedAt: updatedAt || item.annotationUpdatedAt,
|
||||||
}
|
}
|
||||||
: item
|
: item
|
||||||
@@ -586,11 +664,13 @@ export default function LabelStudioTextEditor() {
|
|||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
advanceAfterSave,
|
advanceAfterSave,
|
||||||
|
confirmEmptyAnnotationStatus,
|
||||||
currentSegmentIndex,
|
currentSegmentIndex,
|
||||||
message,
|
message,
|
||||||
projectId,
|
projectId,
|
||||||
segmented,
|
segmented,
|
||||||
selectedFileId,
|
selectedFileId,
|
||||||
|
tasks,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const requestExportForCheck = useCallback(() => {
|
const requestExportForCheck = useCallback(() => {
|
||||||
@@ -1016,37 +1096,37 @@ export default function LabelStudioTextEditor() {
|
|||||||
size="small"
|
size="small"
|
||||||
dataSource={tasks}
|
dataSource={tasks}
|
||||||
loadMore={loadMoreNode}
|
loadMore={loadMoreNode}
|
||||||
renderItem={(item) => (
|
renderItem={(item) => {
|
||||||
<List.Item
|
const statusMeta = resolveTaskStatusMeta(item);
|
||||||
key={item.fileId}
|
return (
|
||||||
className="cursor-pointer hover:bg-blue-50"
|
<List.Item
|
||||||
style={{
|
key={item.fileId}
|
||||||
background: item.fileId === selectedFileId ? "#e6f4ff" : undefined,
|
className="cursor-pointer hover:bg-blue-50"
|
||||||
padding: "8px 12px",
|
style={{
|
||||||
borderBottom: "1px solid #f0f0f0",
|
background: item.fileId === selectedFileId ? "#e6f4ff" : undefined,
|
||||||
}}
|
padding: "8px 12px",
|
||||||
onClick={() => setSelectedFileId(item.fileId)}
|
borderBottom: "1px solid #f0f0f0",
|
||||||
>
|
}}
|
||||||
<div className="flex flex-col w-full gap-1">
|
onClick={() => setSelectedFileId(item.fileId)}
|
||||||
<Typography.Text ellipsis style={{ fontSize: 13 }}>
|
>
|
||||||
{item.fileName}
|
<div className="flex flex-col w-full gap-1">
|
||||||
</Typography.Text>
|
<Typography.Text ellipsis style={{ fontSize: 13 }}>
|
||||||
<div className="flex items-center justify-between">
|
{item.fileName}
|
||||||
<Typography.Text
|
|
||||||
type={item.hasAnnotation ? "success" : "secondary"}
|
|
||||||
style={{ fontSize: 11 }}
|
|
||||||
>
|
|
||||||
{item.hasAnnotation ? "已标注" : "未标注"}
|
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
{item.annotationUpdatedAt && (
|
<div className="flex items-center justify-between">
|
||||||
<Typography.Text type="secondary" style={{ fontSize: 10 }}>
|
<Typography.Text type={statusMeta.type} style={{ fontSize: 11 }}>
|
||||||
{item.annotationUpdatedAt}
|
{statusMeta.text}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
)}
|
{item.annotationUpdatedAt && (
|
||||||
|
<Typography.Text type="secondary" style={{ fontSize: 10 }}>
|
||||||
|
{item.annotationUpdatedAt}
|
||||||
|
</Typography.Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</List.Item>
|
||||||
</List.Item>
|
);
|
||||||
)}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{segmented && (
|
{segmented && (
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ export enum AnnotationTaskStatus {
|
|||||||
SKIPPED = "skipped",
|
SKIPPED = "skipped",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum AnnotationResultStatus {
|
||||||
|
ANNOTATED = "ANNOTATED",
|
||||||
|
NO_ANNOTATION = "NO_ANNOTATION",
|
||||||
|
NOT_APPLICABLE = "NOT_APPLICABLE",
|
||||||
|
}
|
||||||
|
|
||||||
export interface AnnotationTask {
|
export interface AnnotationTask {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -52,7 +58,7 @@ export interface ObjectDefinition {
|
|||||||
export interface TemplateConfiguration {
|
export interface TemplateConfiguration {
|
||||||
labels: LabelDefinition[];
|
labels: LabelDefinition[];
|
||||||
objects: ObjectDefinition[];
|
objects: ObjectDefinition[];
|
||||||
metadata?: Record<string, any>;
|
metadata?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AnnotationTemplate {
|
export interface AnnotationTemplate {
|
||||||
|
|||||||
@@ -2,11 +2,20 @@
|
|||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
from sqlalchemy import Column, String, Boolean, TIMESTAMP, Text, Integer, JSON, ForeignKey, UniqueConstraint, Index
|
from sqlalchemy import Column, String, Boolean, TIMESTAMP, Text, Integer, JSON, ForeignKey, UniqueConstraint, Index
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
from app.db.session import Base
|
from app.db.session import Base
|
||||||
|
|
||||||
class AnnotationTemplate(Base):
|
ANNOTATION_STATUS_ANNOTATED = "ANNOTATED"
|
||||||
|
ANNOTATION_STATUS_NO_ANNOTATION = "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):
|
||||||
"""标注配置模板模型"""
|
"""标注配置模板模型"""
|
||||||
|
|
||||||
__tablename__ = "t_dm_annotation_templates"
|
__tablename__ = "t_dm_annotation_templates"
|
||||||
@@ -84,12 +93,18 @@ class AnnotationResult(Base):
|
|||||||
|
|
||||||
__tablename__ = "t_dm_annotation_results"
|
__tablename__ = "t_dm_annotation_results"
|
||||||
|
|
||||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), comment="UUID")
|
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), comment="UUID")
|
||||||
project_id = Column(String(36), nullable=False, comment="标注项目ID(t_dm_labeling_projects.id)")
|
project_id = Column(String(36), nullable=False, comment="标注项目ID(t_dm_labeling_projects.id)")
|
||||||
file_id = Column(String(36), nullable=False, comment="文件ID(t_dm_dataset_files.id)")
|
file_id = Column(String(36), nullable=False, comment="文件ID(t_dm_dataset_files.id)")
|
||||||
annotation = Column(JSON, nullable=False, comment="Label Studio annotation 原始JSON(单人单份最终结果)")
|
annotation = Column(JSON, nullable=False, comment="Label Studio annotation 原始JSON(单人单份最终结果)")
|
||||||
created_at = Column(TIMESTAMP, server_default=func.current_timestamp(), comment="创建时间")
|
annotation_status = Column(
|
||||||
updated_at = Column(TIMESTAMP, server_default=func.current_timestamp(), onupdate=func.current_timestamp(), comment="更新时间")
|
String(32),
|
||||||
|
nullable=False,
|
||||||
|
default=ANNOTATION_STATUS_ANNOTATED,
|
||||||
|
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="更新时间")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<AnnotationResult(id={self.id}, project_id={self.project_id}, file_id={self.file_id})>"
|
return f"<AnnotationResult(id={self.id}, project_id={self.project_id}, file_id={self.file_id})>"
|
||||||
|
|||||||
@@ -9,10 +9,25 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, ConfigDict
|
from pydantic import BaseModel, Field, ConfigDict
|
||||||
|
|
||||||
|
from app.db.models.annotation_management import (
|
||||||
|
ANNOTATION_STATUS_ANNOTATED,
|
||||||
|
ANNOTATION_STATUS_NO_ANNOTATION,
|
||||||
|
ANNOTATION_STATUS_NOT_APPLICABLE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AnnotationStatus(str, Enum):
|
||||||
|
"""标注状态枚举"""
|
||||||
|
|
||||||
|
ANNOTATED = ANNOTATION_STATUS_ANNOTATED
|
||||||
|
NO_ANNOTATION = ANNOTATION_STATUS_NO_ANNOTATION
|
||||||
|
NOT_APPLICABLE = ANNOTATION_STATUS_NOT_APPLICABLE
|
||||||
|
|
||||||
|
|
||||||
class EditorProjectInfo(BaseModel):
|
class EditorProjectInfo(BaseModel):
|
||||||
"""编辑器项目元信息"""
|
"""编辑器项目元信息"""
|
||||||
@@ -40,8 +55,13 @@ class EditorTaskListItem(BaseModel):
|
|||||||
file_type: Optional[str] = Field(None, alias="fileType", description="文件类型")
|
file_type: Optional[str] = Field(None, alias="fileType", description="文件类型")
|
||||||
has_annotation: bool = Field(..., alias="hasAnnotation", description="是否已有最终标注")
|
has_annotation: bool = Field(..., alias="hasAnnotation", description="是否已有最终标注")
|
||||||
annotation_updated_at: Optional[datetime] = Field(None, alias="annotationUpdatedAt", description="标注更新时间")
|
annotation_updated_at: Optional[datetime] = Field(None, alias="annotationUpdatedAt", description="标注更新时间")
|
||||||
|
annotation_status: Optional[AnnotationStatus] = Field(
|
||||||
|
None,
|
||||||
|
alias="annotationStatus",
|
||||||
|
description="标注状态",
|
||||||
|
)
|
||||||
|
|
||||||
model_config = ConfigDict(populate_by_name=True)
|
model_config = ConfigDict(populate_by_name=True, use_enum_values=True)
|
||||||
|
|
||||||
|
|
||||||
class EditorTaskListResponse(BaseModel):
|
class EditorTaskListResponse(BaseModel):
|
||||||
@@ -89,6 +109,11 @@ class UpsertAnnotationRequest(BaseModel):
|
|||||||
"""保存/覆盖最终标注(Label Studio annotation 原始对象)"""
|
"""保存/覆盖最终标注(Label Studio annotation 原始对象)"""
|
||||||
|
|
||||||
annotation: Dict[str, Any] = Field(..., description="Label Studio annotation 对象(包含 result 等)")
|
annotation: Dict[str, Any] = Field(..., description="Label Studio annotation 对象(包含 result 等)")
|
||||||
|
annotation_status: Optional[AnnotationStatus] = Field(
|
||||||
|
None,
|
||||||
|
alias="annotationStatus",
|
||||||
|
description="标注状态(无标注传 NO_ANNOTATION,不适用传 NOT_APPLICABLE)",
|
||||||
|
)
|
||||||
expected_updated_at: Optional[datetime] = Field(
|
expected_updated_at: Optional[datetime] = Field(
|
||||||
None,
|
None,
|
||||||
alias="expectedUpdatedAt",
|
alias="expectedUpdatedAt",
|
||||||
@@ -101,7 +126,7 @@ class UpsertAnnotationRequest(BaseModel):
|
|||||||
description="段落索引(分段模式下必填)",
|
description="段落索引(分段模式下必填)",
|
||||||
)
|
)
|
||||||
|
|
||||||
model_config = ConfigDict(populate_by_name=True)
|
model_config = ConfigDict(populate_by_name=True, use_enum_values=True)
|
||||||
|
|
||||||
|
|
||||||
class UpsertAnnotationResponse(BaseModel):
|
class UpsertAnnotationResponse(BaseModel):
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.logging import get_logger
|
from app.core.logging import get_logger
|
||||||
from app.db.models import AnnotationResult, Dataset, DatasetFiles, LabelingProject, LabelingProjectFile
|
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_NOT_APPLICABLE,
|
||||||
|
ANNOTATION_STATUS_VALUES,
|
||||||
|
)
|
||||||
from app.module.annotation.config import LabelStudioTagConfig
|
from app.module.annotation.config import LabelStudioTagConfig
|
||||||
from app.module.annotation.schema.editor import (
|
from app.module.annotation.schema.editor import (
|
||||||
EditorProjectInfo,
|
EditorProjectInfo,
|
||||||
@@ -348,6 +354,24 @@ class AnnotationEditorService:
|
|||||||
|
|
||||||
return ET.tostring(root, encoding="unicode")
|
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
|
@classmethod
|
||||||
def _build_source_document_filter(cls):
|
def _build_source_document_filter(cls):
|
||||||
file_type_lower = func.lower(DatasetFiles.file_type)
|
file_type_lower = func.lower(DatasetFiles.file_type)
|
||||||
@@ -447,7 +471,12 @@ class AnnotationEditorService:
|
|||||||
else_=0,
|
else_=0,
|
||||||
)
|
)
|
||||||
files_result = await self.db.execute(
|
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)
|
.join(LabelingProjectFile, LabelingProjectFile.file_id == DatasetFiles.id)
|
||||||
.outerjoin(
|
.outerjoin(
|
||||||
AnnotationResult,
|
AnnotationResult,
|
||||||
@@ -462,7 +491,7 @@ class AnnotationEditorService:
|
|||||||
rows = files_result.all()
|
rows = files_result.all()
|
||||||
|
|
||||||
items: List[EditorTaskListItem] = []
|
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]
|
fid = str(file_record.id) # type: ignore[arg-type]
|
||||||
items.append(
|
items.append(
|
||||||
EditorTaskListItem(
|
EditorTaskListItem(
|
||||||
@@ -471,6 +500,7 @@ class AnnotationEditorService:
|
|||||||
fileType=getattr(file_record, "file_type", None),
|
fileType=getattr(file_record, "file_type", None),
|
||||||
hasAnnotation=annotation_id is not None,
|
hasAnnotation=annotation_id is not None,
|
||||||
annotationUpdatedAt=annotation_updated_at,
|
annotationUpdatedAt=annotation_updated_at,
|
||||||
|
annotationStatus=annotation_status,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -869,12 +899,28 @@ class AnnotationEditorService:
|
|||||||
annotation_payload["id"] = self._make_ls_annotation_id(project_id, file_id)
|
annotation_payload["id"] = self._make_ls_annotation_id(project_id, file_id)
|
||||||
final_payload = annotation_payload
|
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
|
||||||
|
elif requested_status == ANNOTATION_STATUS_NOT_APPLICABLE:
|
||||||
|
final_status = ANNOTATION_STATUS_NOT_APPLICABLE
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=400, detail="未发现标注内容,请确认无标注/不适用后再保存")
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
if request.expected_updated_at and existing.updated_at:
|
if request.expected_updated_at and existing.updated_at:
|
||||||
if existing.updated_at != request.expected_updated_at.replace(tzinfo=None):
|
if existing.updated_at != request.expected_updated_at.replace(tzinfo=None):
|
||||||
raise HTTPException(status_code=409, detail="标注已被更新,请刷新后重试")
|
raise HTTPException(status_code=409, detail="标注已被更新,请刷新后重试")
|
||||||
|
|
||||||
existing.annotation = final_payload # type: ignore[assignment]
|
existing.annotation = final_payload # type: ignore[assignment]
|
||||||
|
existing.annotation_status = final_status # type: ignore[assignment]
|
||||||
existing.updated_at = now # type: ignore[assignment]
|
existing.updated_at = now # type: ignore[assignment]
|
||||||
await self.db.commit()
|
await self.db.commit()
|
||||||
await self.db.refresh(existing)
|
await self.db.refresh(existing)
|
||||||
@@ -892,6 +938,7 @@ class AnnotationEditorService:
|
|||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
file_id=file_id,
|
file_id=file_id,
|
||||||
annotation=final_payload,
|
annotation=final_payload,
|
||||||
|
annotation_status=final_status,
|
||||||
created_at=now,
|
created_at=now,
|
||||||
updated_at=now,
|
updated_at=now,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -65,6 +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/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