You've already forked DataMate
feat(annotation): 优化文本标注分段功能实现
- 新增 getEditorTaskSegmentsUsingGet 接口用于获取任务分段信息 - 移除 SegmentInfo 中的 text、start、end 字段,精简数据结构 - 添加 EditorTaskSegmentsResponse 类型定义用于分段摘要响应 - 实现服务端 get_task_segments 方法,支持分段信息查询 - 重构前端组件缓存机制,使用 segmentSummaryFileRef 管理分段状态 - 优化分段构建逻辑,提取 _build_segment_contexts 公共方法 - 调整后端 _build_text_task 方法中的分段处理流程 - 更新 API 类型定义,统一 RequestParams 和 RequestPayload 类型
This commit is contained in:
@@ -6,6 +6,7 @@ import { useNavigate, useParams } from "react-router";
|
||||
import {
|
||||
getEditorProjectInfoUsingGet,
|
||||
getEditorTaskUsingGet,
|
||||
getEditorTaskSegmentsUsingGet,
|
||||
listEditorTasksUsingGet,
|
||||
upsertEditorAnnotationUsingPut,
|
||||
} from "../annotation.api";
|
||||
@@ -38,9 +39,6 @@ type LsfMessage = {
|
||||
|
||||
type SegmentInfo = {
|
||||
idx: number;
|
||||
text: string;
|
||||
start: number;
|
||||
end: number;
|
||||
hasAnnotation: boolean;
|
||||
lineIndex: number;
|
||||
chunkIndex: number;
|
||||
@@ -66,10 +64,16 @@ type EditorTaskPayload = {
|
||||
type EditorTaskResponse = {
|
||||
task?: EditorTaskPayload;
|
||||
segmented?: boolean;
|
||||
segments?: SegmentInfo[];
|
||||
totalSegments?: number;
|
||||
currentSegmentIndex?: number;
|
||||
};
|
||||
|
||||
type EditorTaskSegmentsResponse = {
|
||||
segmented?: boolean;
|
||||
segments?: SegmentInfo[];
|
||||
totalSegments?: number;
|
||||
};
|
||||
|
||||
type EditorTaskListResponse = {
|
||||
content?: EditorTaskListItem[];
|
||||
totalElements?: number;
|
||||
@@ -288,6 +292,7 @@ export default function LabelStudioTextEditor() {
|
||||
const segmentStatsCacheRef = useRef<Record<string, SegmentStats>>({});
|
||||
const segmentStatsSeqRef = useRef(0);
|
||||
const segmentStatsLoadingRef = useRef<Set<string>>(new Set());
|
||||
const segmentSummaryFileRef = useRef<string>("");
|
||||
|
||||
const [loadingProject, setLoadingProject] = useState(true);
|
||||
const [loadingTasks, setLoadingTasks] = useState(false);
|
||||
@@ -358,9 +363,7 @@ export default function LabelStudioTextEditor() {
|
||||
if (segmentStatsCacheRef.current[fileId] || segmentStatsLoadingRef.current.has(fileId)) return;
|
||||
segmentStatsLoadingRef.current.add(fileId);
|
||||
try {
|
||||
const resp = (await getEditorTaskUsingGet(projectId, fileId, {
|
||||
segmentIndex: 0,
|
||||
})) as ApiResponse<EditorTaskResponse>;
|
||||
const resp = (await getEditorTaskSegmentsUsingGet(projectId, fileId)) as ApiResponse<EditorTaskSegmentsResponse>;
|
||||
if (segmentStatsSeqRef.current !== seq) return;
|
||||
const data = resp?.data;
|
||||
if (!data?.segmented) return;
|
||||
@@ -591,20 +594,38 @@ export default function LabelStudioTextEditor() {
|
||||
if (seq !== initSeqRef.current) return;
|
||||
|
||||
// 更新分段状态
|
||||
const segmentIndex = data?.segmented
|
||||
const isSegmented = !!data?.segmented;
|
||||
const segmentIndex = isSegmented
|
||||
? resolveSegmentIndex(data.currentSegmentIndex) ?? 0
|
||||
: undefined;
|
||||
if (data?.segmented) {
|
||||
const stats = buildSegmentStats(data.segments);
|
||||
if (isSegmented) {
|
||||
let nextSegments: SegmentInfo[] = [];
|
||||
if (segmentSummaryFileRef.current === fileId && segments.length > 0) {
|
||||
nextSegments = segments;
|
||||
} else {
|
||||
try {
|
||||
const segmentResp = (await getEditorTaskSegmentsUsingGet(projectId, fileId)) as ApiResponse<EditorTaskSegmentsResponse>;
|
||||
if (seq !== initSeqRef.current) return;
|
||||
const segmentData = segmentResp?.data;
|
||||
if (segmentData?.segmented) {
|
||||
nextSegments = Array.isArray(segmentData.segments) ? segmentData.segments : [];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
const stats = buildSegmentStats(nextSegments);
|
||||
setSegmented(true);
|
||||
setSegments(data.segments || []);
|
||||
setSegments(nextSegments);
|
||||
setCurrentSegmentIndex(segmentIndex ?? 0);
|
||||
updateSegmentStatsCache(fileId, stats);
|
||||
segmentSummaryFileRef.current = fileId;
|
||||
} else {
|
||||
setSegmented(false);
|
||||
setSegments([]);
|
||||
setCurrentSegmentIndex(0);
|
||||
updateSegmentStatsCache(fileId, null);
|
||||
segmentSummaryFileRef.current = fileId;
|
||||
}
|
||||
|
||||
const taskData = {
|
||||
@@ -664,7 +685,7 @@ export default function LabelStudioTextEditor() {
|
||||
} finally {
|
||||
if (seq === initSeqRef.current) setLoadingTaskDetail(false);
|
||||
}
|
||||
}, [iframeReady, message, postToIframe, project, projectId, updateSegmentStatsCache]);
|
||||
}, [iframeReady, message, postToIframe, project, projectId, segments, updateSegmentStatsCache]);
|
||||
|
||||
const advanceAfterSave = useCallback(async (fileId: string, segmentIndex?: number) => {
|
||||
if (!fileId) return;
|
||||
@@ -979,6 +1000,7 @@ export default function LabelStudioTextEditor() {
|
||||
setSegmented(false);
|
||||
setSegments([]);
|
||||
setCurrentSegmentIndex(0);
|
||||
segmentSummaryFileRef.current = "";
|
||||
savedSnapshotsRef.current = {};
|
||||
segmentStatsSeqRef.current += 1;
|
||||
segmentStatsCacheRef.current = {};
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import { get, post, put, del, download } from "@/utils/request";
|
||||
|
||||
// 导出格式类型
|
||||
export type ExportFormat = "json" | "jsonl" | "csv" | "coco" | "yolo";
|
||||
|
||||
// 标注任务管理相关接口
|
||||
export function queryAnnotationTasksUsingGet(params?: any) {
|
||||
return get("/api/annotation/project", params);
|
||||
}
|
||||
|
||||
export function createAnnotationTaskUsingPost(data: any) {
|
||||
return post("/api/annotation/project", data);
|
||||
}
|
||||
|
||||
export function syncAnnotationTaskUsingPost(data: any) {
|
||||
return post(`/api/annotation/task/sync`, data);
|
||||
}
|
||||
// 导出格式类型
|
||||
export type ExportFormat = "json" | "jsonl" | "csv" | "coco" | "yolo";
|
||||
|
||||
type RequestParams = Record<string, unknown>;
|
||||
type RequestPayload = Record<string, unknown>;
|
||||
|
||||
// 标注任务管理相关接口
|
||||
export function queryAnnotationTasksUsingGet(params?: RequestParams) {
|
||||
return get("/api/annotation/project", params);
|
||||
}
|
||||
|
||||
export function createAnnotationTaskUsingPost(data: RequestPayload) {
|
||||
return post("/api/annotation/project", data);
|
||||
}
|
||||
|
||||
export function syncAnnotationTaskUsingPost(data: RequestPayload) {
|
||||
return post(`/api/annotation/task/sync`, data);
|
||||
}
|
||||
|
||||
export function deleteAnnotationTaskByIdUsingDelete(mappingId: string) {
|
||||
// Backend expects mapping UUID as path parameter
|
||||
@@ -25,9 +28,9 @@ export function getAnnotationTaskByIdUsingGet(taskId: string) {
|
||||
return get(`/api/annotation/project/${taskId}`);
|
||||
}
|
||||
|
||||
export function updateAnnotationTaskByIdUsingPut(taskId: string, data: any) {
|
||||
return put(`/api/annotation/project/${taskId}`, data);
|
||||
}
|
||||
export function updateAnnotationTaskByIdUsingPut(taskId: string, data: RequestPayload) {
|
||||
return put(`/api/annotation/project/${taskId}`, data);
|
||||
}
|
||||
|
||||
// 标签配置管理
|
||||
export function getTagConfigUsingGet() {
|
||||
@@ -35,20 +38,20 @@ export function getTagConfigUsingGet() {
|
||||
}
|
||||
|
||||
// 标注模板管理
|
||||
export function queryAnnotationTemplatesUsingGet(params?: any) {
|
||||
return get("/api/annotation/template", params);
|
||||
}
|
||||
export function queryAnnotationTemplatesUsingGet(params?: RequestParams) {
|
||||
return get("/api/annotation/template", params);
|
||||
}
|
||||
|
||||
export function createAnnotationTemplateUsingPost(data: RequestPayload) {
|
||||
return post("/api/annotation/template", data);
|
||||
}
|
||||
|
||||
export function createAnnotationTemplateUsingPost(data: any) {
|
||||
return post("/api/annotation/template", data);
|
||||
}
|
||||
|
||||
export function updateAnnotationTemplateByIdUsingPut(
|
||||
templateId: string | number,
|
||||
data: any
|
||||
) {
|
||||
return put(`/api/annotation/template/${templateId}`, data);
|
||||
}
|
||||
export function updateAnnotationTemplateByIdUsingPut(
|
||||
templateId: string | number,
|
||||
data: RequestPayload
|
||||
) {
|
||||
return put(`/api/annotation/template/${templateId}`, data);
|
||||
}
|
||||
|
||||
export function deleteAnnotationTemplateByIdUsingDelete(
|
||||
templateId: string | number
|
||||
@@ -65,27 +68,31 @@ export function getEditorProjectInfoUsingGet(projectId: string) {
|
||||
return get(`/api/annotation/editor/projects/${projectId}`);
|
||||
}
|
||||
|
||||
export function listEditorTasksUsingGet(projectId: string, params?: any) {
|
||||
return get(`/api/annotation/editor/projects/${projectId}/tasks`, params);
|
||||
}
|
||||
export function listEditorTasksUsingGet(projectId: string, params?: RequestParams) {
|
||||
return get(`/api/annotation/editor/projects/${projectId}/tasks`, params);
|
||||
}
|
||||
|
||||
export function getEditorTaskUsingGet(
|
||||
projectId: string,
|
||||
fileId: string,
|
||||
params?: { segmentIndex?: number }
|
||||
) {
|
||||
return get(`/api/annotation/editor/projects/${projectId}/tasks/${fileId}`, params);
|
||||
}
|
||||
export function getEditorTaskUsingGet(
|
||||
projectId: string,
|
||||
fileId: string,
|
||||
params?: { segmentIndex?: number }
|
||||
) {
|
||||
return get(`/api/annotation/editor/projects/${projectId}/tasks/${fileId}`, params);
|
||||
}
|
||||
|
||||
export function getEditorTaskSegmentsUsingGet(projectId: string, fileId: string) {
|
||||
return get(`/api/annotation/editor/projects/${projectId}/tasks/${fileId}/segments`);
|
||||
}
|
||||
|
||||
export function upsertEditorAnnotationUsingPut(
|
||||
projectId: string,
|
||||
fileId: string,
|
||||
data: {
|
||||
annotation: any;
|
||||
expectedUpdatedAt?: string;
|
||||
segmentIndex?: number;
|
||||
}
|
||||
) {
|
||||
projectId: string,
|
||||
fileId: string,
|
||||
data: {
|
||||
annotation: Record<string, unknown>;
|
||||
expectedUpdatedAt?: string;
|
||||
segmentIndex?: number;
|
||||
}
|
||||
) {
|
||||
return put(`/api/annotation/editor/projects/${projectId}/tasks/${fileId}/annotation`, data);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user