You've already forked DataMate
feat(annotation): 文件版本更新时支持保留标注记录(位置偏移+文字匹配迁移)
新增 AnnotationMigrator 迁移算法,在 TEXT 类型数据集的文件版本更新时, 可选通过 difflib 位置偏移映射和文字二次匹配将旧版本标注迁移到新版本上。 前端版本切换对话框增加"保留标注"复选框(仅 TEXT 类型显示),后端 API 增加 preserveAnnotations 参数,完全向后兼容。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { App, Button, Card, List, Spin, Typography, Tag, Empty } from "antd";
|
||||
import { App, Button, Card, Checkbox, List, Spin, Typography, Tag, Empty } from "antd";
|
||||
import { LeftOutlined, ReloadOutlined, SaveOutlined, MenuFoldOutlined, MenuUnfoldOutlined } from "@ant-design/icons";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
checkFileVersionUsingGet,
|
||||
applyNewVersionUsingPost,
|
||||
type FileVersionCheckResponse,
|
||||
type UseNewVersionResponse,
|
||||
} from "../annotation.api";
|
||||
import { AnnotationResultStatus } from "../annotation.model";
|
||||
|
||||
@@ -242,6 +243,7 @@ export default function LabelStudioTextEditor() {
|
||||
} | null>(null);
|
||||
const savedSnapshotsRef = useRef<Record<string, string>>({});
|
||||
const pendingAutoAdvanceRef = useRef(false);
|
||||
const preserveAnnotationsRef = useRef(true);
|
||||
|
||||
const [loadingProject, setLoadingProject] = useState(true);
|
||||
const [loadingTasks, setLoadingTasks] = useState(false);
|
||||
@@ -594,18 +596,31 @@ export default function LabelStudioTextEditor() {
|
||||
const handleUseNewVersion = useCallback(async () => {
|
||||
if (!selectedFileId) return;
|
||||
|
||||
// Reset ref to default before opening dialog
|
||||
preserveAnnotationsRef.current = true;
|
||||
|
||||
modal.confirm({
|
||||
title: "确认使用新版本",
|
||||
content: (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Typography.Text>
|
||||
确认使用新版本?这将清空当前标注并使用最新版本的文件内容。
|
||||
确认使用新版本?这将使用最新版本的文件内容。
|
||||
</Typography.Text>
|
||||
{fileVersionInfo && (
|
||||
<Typography.Text type="secondary">
|
||||
当前标注版本: {fileVersionInfo.annotationFileVersion},最新文件版本: {fileVersionInfo.currentFileVersion}
|
||||
</Typography.Text>
|
||||
)}
|
||||
{isTextProject && (
|
||||
<Checkbox
|
||||
defaultChecked={true}
|
||||
onChange={(e) => {
|
||||
preserveAnnotationsRef.current = e.target.checked;
|
||||
}}
|
||||
>
|
||||
尝试保留已有标注(根据文字匹配迁移)
|
||||
</Checkbox>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
okText: "确认",
|
||||
@@ -615,8 +630,19 @@ export default function LabelStudioTextEditor() {
|
||||
if (!projectId || !selectedFileId) return;
|
||||
setUsingNewVersion(true);
|
||||
try {
|
||||
await applyNewVersionUsingPost(projectId, selectedFileId);
|
||||
message.success("已使用新版本并清空标注");
|
||||
const resp = (await applyNewVersionUsingPost(
|
||||
projectId,
|
||||
selectedFileId,
|
||||
preserveAnnotationsRef.current,
|
||||
)) as ApiResponse<UseNewVersionResponse>;
|
||||
const data = resp?.data;
|
||||
if (data?.migratedCount != null) {
|
||||
message.success(
|
||||
`已切换到新版本,${data.migratedCount} 条标注已迁移,${data.failedCount ?? 0} 条无法迁移`,
|
||||
);
|
||||
} else {
|
||||
message.success("已使用新版本并清空标注");
|
||||
}
|
||||
setFileVersionInfo(null);
|
||||
await loadTasks({ mode: "reset" });
|
||||
await initEditorForFile(selectedFileId);
|
||||
@@ -628,7 +654,7 @@ export default function LabelStudioTextEditor() {
|
||||
}
|
||||
},
|
||||
});
|
||||
}, [modal, message, projectId, selectedFileId, fileVersionInfo, loadTasks, initEditorForFile]);
|
||||
}, [modal, message, projectId, selectedFileId, fileVersionInfo, isTextProject, loadTasks, initEditorForFile]);
|
||||
|
||||
const advanceAfterSave = useCallback(async (fileId: string, segmentIndex?: number) => {
|
||||
if (!fileId) return;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { get, post, put, del, download } from "@/utils/request";
|
||||
|
||||
import { get, post, put, del, download } from "@/utils/request";
|
||||
|
||||
// 导出格式类型
|
||||
export type ExportFormat = "json" | "jsonl" | "csv" | "coco" | "yolo";
|
||||
|
||||
@@ -42,26 +42,26 @@ export function stopAnnotationOperatorTaskByIdUsingPost(taskId: string) {
|
||||
export function downloadAnnotationOperatorTaskResultUsingGet(taskId: string, filename?: string) {
|
||||
return download(`/api/annotation/operator-tasks/${taskId}/download`, null, filename);
|
||||
}
|
||||
|
||||
export function deleteAnnotationTaskByIdUsingDelete(mappingId: string) {
|
||||
// Backend expects mapping UUID as path parameter
|
||||
return del(`/api/annotation/project/${mappingId}`);
|
||||
}
|
||||
|
||||
export function getAnnotationTaskByIdUsingGet(taskId: string) {
|
||||
return get(`/api/annotation/project/${taskId}`);
|
||||
}
|
||||
|
||||
|
||||
export function deleteAnnotationTaskByIdUsingDelete(mappingId: string) {
|
||||
// Backend expects mapping UUID as path parameter
|
||||
return del(`/api/annotation/project/${mappingId}`);
|
||||
}
|
||||
|
||||
export function getAnnotationTaskByIdUsingGet(taskId: string) {
|
||||
return get(`/api/annotation/project/${taskId}`);
|
||||
}
|
||||
|
||||
export function updateAnnotationTaskByIdUsingPut(taskId: string, data: RequestPayload) {
|
||||
return put(`/api/annotation/project/${taskId}`, data);
|
||||
}
|
||||
|
||||
// 标签配置管理
|
||||
export function getTagConfigUsingGet() {
|
||||
return get("/api/annotation/tags/config");
|
||||
}
|
||||
|
||||
// 标注模板管理
|
||||
|
||||
// 标签配置管理
|
||||
export function getTagConfigUsingGet() {
|
||||
return get("/api/annotation/tags/config");
|
||||
}
|
||||
|
||||
// 标注模板管理
|
||||
export function queryAnnotationTemplatesUsingGet(params?: RequestParams) {
|
||||
return get("/api/annotation/template", params);
|
||||
}
|
||||
@@ -69,33 +69,33 @@ export function queryAnnotationTemplatesUsingGet(params?: RequestParams) {
|
||||
export function createAnnotationTemplateUsingPost(data: RequestPayload) {
|
||||
return post("/api/annotation/template", data);
|
||||
}
|
||||
|
||||
|
||||
export function updateAnnotationTemplateByIdUsingPut(
|
||||
templateId: string | number,
|
||||
data: RequestPayload
|
||||
) {
|
||||
return put(`/api/annotation/template/${templateId}`, data);
|
||||
}
|
||||
|
||||
export function deleteAnnotationTemplateByIdUsingDelete(
|
||||
templateId: string | number
|
||||
) {
|
||||
return del(`/api/annotation/template/${templateId}`);
|
||||
}
|
||||
|
||||
|
||||
// =====================
|
||||
// Label Studio Editor(内嵌版)
|
||||
// =====================
|
||||
|
||||
export function getEditorProjectInfoUsingGet(projectId: string) {
|
||||
return get(`/api/annotation/editor/projects/${projectId}`);
|
||||
}
|
||||
|
||||
|
||||
export function deleteAnnotationTemplateByIdUsingDelete(
|
||||
templateId: string | number
|
||||
) {
|
||||
return del(`/api/annotation/template/${templateId}`);
|
||||
}
|
||||
|
||||
|
||||
// =====================
|
||||
// Label Studio Editor(内嵌版)
|
||||
// =====================
|
||||
|
||||
export function getEditorProjectInfoUsingGet(projectId: string) {
|
||||
return get(`/api/annotation/editor/projects/${projectId}`);
|
||||
}
|
||||
|
||||
export function listEditorTasksUsingGet(projectId: string, params?: RequestParams) {
|
||||
return get(`/api/annotation/editor/projects/${projectId}/tasks`, params);
|
||||
}
|
||||
|
||||
|
||||
export function getEditorTaskUsingGet(
|
||||
projectId: string,
|
||||
fileId: string,
|
||||
@@ -111,7 +111,7 @@ export function getEditorTaskSegmentUsingGet(
|
||||
) {
|
||||
return get(`/api/annotation/editor/projects/${projectId}/tasks/${fileId}/segments`, params);
|
||||
}
|
||||
|
||||
|
||||
export function upsertEditorAnnotationUsingPut(
|
||||
projectId: string,
|
||||
fileId: string,
|
||||
@@ -141,40 +141,49 @@ export interface UseNewVersionResponse {
|
||||
previousFileVersion: number | null;
|
||||
currentFileVersion: number;
|
||||
message: string;
|
||||
migratedCount?: number;
|
||||
failedCount?: number;
|
||||
}
|
||||
|
||||
export function applyNewVersionUsingPost(projectId: string, fileId: string) {
|
||||
return post(`/api/annotation/editor/projects/${projectId}/files/${fileId}/use-new-version`, {});
|
||||
export function applyNewVersionUsingPost(
|
||||
projectId: string,
|
||||
fileId: string,
|
||||
preserveAnnotations: boolean = false,
|
||||
) {
|
||||
return post(
|
||||
`/api/annotation/editor/projects/${projectId}/files/${fileId}/use-new-version`,
|
||||
{ preserveAnnotations },
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// =====================
|
||||
// 标注数据导出
|
||||
// =====================
|
||||
|
||||
export interface ExportStatsResponse {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
totalFiles: number;
|
||||
annotatedFiles: number;
|
||||
exportFormat: string;
|
||||
}
|
||||
|
||||
export function getExportStatsUsingGet(projectId: string) {
|
||||
return get(`/api/annotation/export/projects/${projectId}/stats`);
|
||||
}
|
||||
|
||||
export function downloadAnnotationsUsingGet(
|
||||
projectId: string,
|
||||
format: ExportFormat = "json",
|
||||
onlyAnnotated: boolean = true,
|
||||
includeData: boolean = false,
|
||||
filename?: string
|
||||
) {
|
||||
const params = new URLSearchParams({
|
||||
format,
|
||||
only_annotated: String(onlyAnnotated),
|
||||
include_data: String(includeData),
|
||||
});
|
||||
return download(`/api/annotation/export/projects/${projectId}/download?${params.toString()}`, null, filename);
|
||||
}
|
||||
|
||||
// =====================
|
||||
// 标注数据导出
|
||||
// =====================
|
||||
|
||||
export interface ExportStatsResponse {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
totalFiles: number;
|
||||
annotatedFiles: number;
|
||||
exportFormat: string;
|
||||
}
|
||||
|
||||
export function getExportStatsUsingGet(projectId: string) {
|
||||
return get(`/api/annotation/export/projects/${projectId}/stats`);
|
||||
}
|
||||
|
||||
export function downloadAnnotationsUsingGet(
|
||||
projectId: string,
|
||||
format: ExportFormat = "json",
|
||||
onlyAnnotated: boolean = true,
|
||||
includeData: boolean = false,
|
||||
filename?: string
|
||||
) {
|
||||
const params = new URLSearchParams({
|
||||
format,
|
||||
only_annotated: String(onlyAnnotated),
|
||||
include_data: String(includeData),
|
||||
});
|
||||
return download(`/api/annotation/export/projects/${projectId}/download?${params.toString()}`, null, filename);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user