Skip to content

Commit 5e6a418

Browse files
authored
feat(auto-annotation): integrate auto-label results with Label Studio and improve task creation (#230)
-Connect auto-annotation results with Label Studio for in-task visualization -Support displaying pre-annotations after auto-label synchronization -Optimize manual annotation workflow -Enable task creation with multiple datasets and multi-data selection
1 parent 5bd6024 commit 5e6a418

16 files changed

Lines changed: 4188 additions & 2789 deletions

File tree

frontend/src/components/business/DatasetFileTransfer.tsx

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,11 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
121121
...item,
122122
id: item.id,
123123
key: String(item.id), // rowKey 使用字符串,确保与 selectedRowKeys 类型一致
124+
// 记录所属数据集,方便后续在“全不选”时只清空当前数据集的选择
125+
// DatasetFile 接口是后端模型,这里在前端扩展 datasetId 字段
126+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
127+
// @ts-ignore
128+
datasetId: selectedDataset.id,
124129
datasetName: selectedDataset.name,
125130
}))
126131
);
@@ -176,6 +181,10 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
176181
(item: DatasetFile) => ({
177182
...item,
178183
key: item.id,
184+
// 同样为批量全选结果打上 datasetId 标记
185+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
186+
// @ts-ignore
187+
datasetId: selectedDataset.id,
179188
datasetName: selectedDataset.name,
180189
}),
181190
);
@@ -260,9 +269,7 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
260269
<div className="flex items-center gap-2">
261270
<span
262271
className={`inline-flex h-3 w-3 rounded-full border transition-colors duration-150 ${
263-
active
264-
? "border-blue-500 bg-blue-500 shadow-[0_0_0_2px_rgba(59,130,246,0.25)]"
265-
: "border-gray-300 bg-white"
272+
active ? "border-blue-500 bg-blue-500" : "border-gray-300 bg-white"
266273
}`}
267274
/>
268275
<span className="truncate" title={text}>
@@ -394,8 +401,20 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
394401
// 而不是只选中当前页
395402
handleSelectAllInDataset();
396403
} else {
397-
// 取消表头“全选”时,清空当前已选文件
398-
onSelectedFilesChange({});
404+
// 取消表头“全选”时,只清空当前数据集的已选文件,保留其它数据集
405+
if (!selectedDataset) return;
406+
407+
const nextMap: { [key: string]: DatasetFile } = {};
408+
Object.entries(selectedFilesMap).forEach(([key, file]) => {
409+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
410+
// @ts-ignore
411+
const fileDatasetId = file.datasetId;
412+
if (fileDatasetId !== selectedDataset.id) {
413+
nextMap[key] = file;
414+
}
415+
});
416+
417+
onSelectedFilesChange(nextMap);
399418
}
400419
},
401420

frontend/src/pages/DataAnnotation/AutoAnnotation/AutoAnnotation.tsx

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@ import {
66
DownloadOutlined,
77
ReloadOutlined,
88
EyeOutlined,
9+
SyncOutlined,
10+
EditOutlined,
911
} from "@ant-design/icons";
1012
import type { ColumnType } from "antd/es/table";
1113
import type { AutoAnnotationTask, AutoAnnotationStatus } from "../annotation.model";
1214
import {
1315
queryAutoAnnotationTasksUsingGet,
1416
deleteAutoAnnotationTaskByIdUsingDelete,
1517
downloadAutoAnnotationResultUsingGet,
18+
queryAnnotationTasksUsingGet,
19+
syncAutoAnnotationTaskToLabelStudioUsingPost,
1620
} from "../annotation.api";
1721
import CreateAutoAnnotationDialog from "./components/CreateAutoAnnotationDialog";
1822

@@ -45,6 +49,8 @@ export default function AutoAnnotation() {
4549
const [tasks, setTasks] = useState<AutoAnnotationTask[]>([]);
4650
const [showCreateDialog, setShowCreateDialog] = useState(false);
4751
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
52+
const [labelStudioBase, setLabelStudioBase] = useState<string | null>(null);
53+
const [datasetProjectMap, setDatasetProjectMap] = useState<Record<string, string>>({});
4854

4955
useEffect(() => {
5056
fetchTasks();
@@ -54,6 +60,39 @@ export default function AutoAnnotation() {
5460
return () => clearInterval(interval);
5561
}, []);
5662

63+
// 预取 Label Studio 基础 URL 和数据集到项目的映射
64+
useEffect(() => {
65+
let mounted = true;
66+
(async () => {
67+
try {
68+
const baseUrl = `http://${window.location.hostname}:${parseInt(window.location.port) + 1}`;
69+
if (mounted) setLabelStudioBase(baseUrl);
70+
} catch (e) {
71+
if (mounted) setLabelStudioBase(null);
72+
}
73+
74+
// 拉取所有标注任务,构建 datasetId -> labelingProjId 映射
75+
try {
76+
const resp = await queryAnnotationTasksUsingGet({ page: 1, size: 1000 } as any);
77+
const content: any[] = (resp as any)?.data?.content || (resp as any)?.data || resp || [];
78+
const map: Record<string, string> = {};
79+
content.forEach((task: any) => {
80+
const datasetId = task.datasetId || task.dataset_id;
81+
const projId = task.labelingProjId || task.projId || task.labeling_project_id;
82+
if (datasetId && projId) {
83+
map[String(datasetId)] = String(projId);
84+
}
85+
});
86+
if (mounted) setDatasetProjectMap(map);
87+
} catch (e) {
88+
console.error("Failed to build dataset->LabelStudio project map:", e);
89+
}
90+
})();
91+
return () => {
92+
mounted = false;
93+
};
94+
}, []);
95+
5796
const fetchTasks = async (silent = false) => {
5897
if (!silent) setLoading(true);
5998
try {
@@ -101,6 +140,56 @@ export default function AutoAnnotation() {
101140
}
102141
};
103142

143+
const handleSyncToLabelStudio = (task: AutoAnnotationTask) => {
144+
if (task.status !== "completed") {
145+
message.warning("仅已完成的任务可以同步到 Label Studio");
146+
return;
147+
}
148+
149+
Modal.confirm({
150+
title: `确认同步自动标注任务「${task.name}」到 Label Studio 吗?`,
151+
content: (
152+
<div>
153+
<div>将把该任务的检测结果作为预测框写入 Label Studio。</div>
154+
<div>不会覆盖已有人工标注,仅作为可编辑的预测结果。</div>
155+
</div>
156+
),
157+
okText: "同步",
158+
cancelText: "取消",
159+
onOk: async () => {
160+
try {
161+
await syncAutoAnnotationTaskToLabelStudioUsingPost(task.id);
162+
message.success("同步请求已发送");
163+
} catch (error) {
164+
console.error(error);
165+
message.error("同步失败,请稍后重试");
166+
}
167+
},
168+
});
169+
};
170+
171+
const handleAnnotate = (task: AutoAnnotationTask) => {
172+
const datasetId = task.datasetId;
173+
if (!datasetId) {
174+
message.error("该任务未绑定数据集,无法跳转 Label Studio");
175+
return;
176+
}
177+
178+
const projId = datasetProjectMap[String(datasetId)];
179+
if (!projId) {
180+
message.error("未找到对应的标注工程,请先为该数据集创建手动标注任务");
181+
return;
182+
}
183+
184+
if (!labelStudioBase) {
185+
message.error("无法跳转到 Label Studio:未配置 Label Studio 基础 URL");
186+
return;
187+
}
188+
189+
const target = `${labelStudioBase}/projects/${projId}/data`;
190+
window.open(target, "_blank");
191+
};
192+
104193
const handleViewResult = (task: AutoAnnotationTask) => {
105194
if (task.outputPath) {
106195
Modal.info({
@@ -214,7 +303,7 @@ export default function AutoAnnotation() {
214303
{
215304
title: "操作",
216305
key: "actions",
217-
width: 180,
306+
width: 260,
218307
fixed: "right",
219308
render: (_: any, record: AutoAnnotationTask) => (
220309
<Space size="small">
@@ -236,9 +325,25 @@ export default function AutoAnnotation() {
236325
onClick={() => handleDownload(record)}
237326
/>
238327
</Tooltip>
328+
<Tooltip title="同步到 Label Studio">
329+
<Button
330+
type="link"
331+
size="small"
332+
icon={<SyncOutlined />}
333+
onClick={() => handleSyncToLabelStudio(record)}
334+
/>
335+
</Tooltip>
336+
<Tooltip title="在 Label Studio 中标注">
337+
<Button
338+
type="link"
339+
size="small"
340+
icon={<EditOutlined />}
341+
onClick={() => handleAnnotate(record)}
342+
/>
343+
</Tooltip>
239344
</>
240345
)}
241-
<Tooltip title="删除">
346+
<Tooltip title="删除任务记录">
242347
<Button
243348
type="link"
244349
size="small"

frontend/src/pages/DataAnnotation/AutoAnnotation/components/CreateAutoAnnotationDialog.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,20 @@ export default function CreateAutoAnnotationDialog({
158158

159159
setLoading(true);
160160

161+
const selectedFiles = Object.values(selectedFilesMap) as any[];
162+
// 自动标注任务现在允许跨多个数据集,后端会按 fileIds 分组并为每个数据集分别创建/复用 LS 项目。
163+
// 这里仅用第一个涉及到的 datasetId(或表单中的 datasetId)作为任务的“主数据集”展示字段。
164+
const datasetIds = Array.from(
165+
new Set(
166+
selectedFiles
167+
.map((file) => file?.datasetId)
168+
.filter((id) => id !== undefined && id !== null && id !== ""),
169+
),
170+
),
171+
);
172+
173+
const effectiveDatasetId = values.datasetId || datasetIds[0];
174+
161175
const imageExtensions = [".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tiff", ".webp"];
162176
const imageFileIds = Object.values(selectedFilesMap)
163177
.filter((file) => {
@@ -168,7 +182,7 @@ export default function CreateAutoAnnotationDialog({
168182

169183
const payload = {
170184
name: values.name,
171-
datasetId: values.datasetId,
185+
datasetId: effectiveDatasetId,
172186
fileIds: imageFileIds,
173187
config: {
174188
modelSize: values.modelSize,

0 commit comments

Comments
 (0)