Skip to content

Commit 61b1a9b

Browse files
authored
feat(annotation): refine sync behavior and add annotation export option (#299)
1 parent a804b4e commit 61b1a9b

15 files changed

Lines changed: 687 additions & 125 deletions

File tree

backend/services/data-management-service/src/main/java/com/datamate/datamanagement/interfaces/dto/DatasetFileResponse.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ public class DatasetFileResponse {
3131
private String tags;
3232
/** 标签更新时间 */
3333
private LocalDateTime tagsUpdatedAt;
34+
/** 文件元数据(包含标注信息等,JSON 字符串) */
35+
private String metadata;
3436
/** 上传时间 */
3537
private LocalDateTime uploadTime;
3638
/** 最后更新时间 */

frontend/src/components/business/DatasetFileTransfer.tsx

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ interface DatasetFileTransferProps
2121
onSelectedFilesChange: (filesMap: { [key: string]: DatasetFile }) => void;
2222
onDatasetSelect?: (dataset: Dataset | null) => void;
2323
datasetTypeFilter?: DatasetType;
24+
/**
25+
* 允许选择的文件扩展名白名单(小写,包含点号,例如 ".jpg")。
26+
* - 若不设置,则不过滤扩展名;
27+
* - 若设置,则仅展示和选择这些扩展名的文件(包括“全选当前数据集”)。
28+
*/
29+
allowedFileExtensions?: string[];
2430
/**
2531
* 是否强制“单数据集模式”:
2632
* - 为 true 时,仅允许从同一个数据集选择文件;
@@ -77,6 +83,7 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
7783
onSelectedFilesChange,
7884
onDatasetSelect,
7985
datasetTypeFilter,
86+
allowedFileExtensions,
8087
singleDatasetOnly,
8188
fixedDatasetId,
8289
lockedFileIds,
@@ -180,27 +187,36 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
180187
size: pageSize,
181188
keyword,
182189
});
183-
setFiles(
184-
(data.content || []).map((item: DatasetFile) => ({
185-
...item,
186-
id: item.id,
187-
key: String(item.id), // rowKey 使用字符串,确保与 selectedRowKeys 类型一致
188-
// 记录所属数据集,方便后续在“全不选”时只清空当前数据集的选择
189-
// DatasetFile 接口是后端模型,这里在前端扩展 datasetId 字段
190-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
191-
// @ts-ignore
192-
datasetId: selectedDataset.id,
193-
datasetName: selectedDataset.name,
194-
}))
195-
);
190+
const mapped = (data.content || []).map((item: DatasetFile) => ({
191+
...item,
192+
id: item.id,
193+
key: String(item.id), // rowKey 使用字符串,确保与 selectedRowKeys 类型一致
194+
// 记录所属数据集,方便后续在“全不选”时只清空当前数据集的选择
195+
// DatasetFile 接口是后端模型,这里在前端扩展 datasetId 字段
196+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
197+
// @ts-ignore
198+
datasetId: selectedDataset.id,
199+
datasetName: selectedDataset.name,
200+
}));
201+
202+
const filtered =
203+
allowedFileExtensions && allowedFileExtensions.length > 0
204+
? mapped.filter((file) => {
205+
const ext =
206+
file.fileName?.toLowerCase().match(/\.[^.]+$/)?.[0] || "";
207+
return allowedFileExtensions.includes(ext);
208+
})
209+
: mapped;
210+
211+
setFiles(filtered);
196212
setFilesPagination((prev) => ({
197213
...prev,
198214
current: page,
199215
pageSize,
200216
total: data.totalElements,
201217
}));
202218
},
203-
[selectedDataset, filesPagination.current, filesPagination.pageSize, filesSearch]
219+
[selectedDataset, filesPagination.current, filesPagination.pageSize, filesSearch, allowedFileExtensions]
204220
);
205221

206222
useEffect(() => {
@@ -269,7 +285,7 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
269285
size: pageSize,
270286
});
271287

272-
const content: DatasetFile[] = (data.content || []).map(
288+
const mapped: DatasetFile[] = (data.content || []).map(
273289
(item: DatasetFile) => ({
274290
...item,
275291
key: item.id,
@@ -281,6 +297,15 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
281297
}),
282298
);
283299

300+
const content: DatasetFile[] =
301+
allowedFileExtensions && allowedFileExtensions.length > 0
302+
? mapped.filter((file) => {
303+
const ext =
304+
file.fileName?.toLowerCase().match(/\.[^.]+$/)?.[0] || "";
305+
return allowedFileExtensions.includes(ext);
306+
})
307+
: mapped;
308+
284309
if (!content.length) {
285310
break;
286311
}
@@ -306,7 +331,7 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
306331

307332
onSelectedFilesChange(newMap);
308333

309-
const count = total || allFiles.length;
334+
const count = allFiles.length;
310335
if (count > 0) {
311336
message.success(`已选中当前数据集的全部 ${count} 个文件`);
312337
} else {

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

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
deleteAutoAnnotationTaskByIdUsingDelete,
1919
downloadAutoAnnotationResultUsingGet,
2020
queryAnnotationTasksUsingGet,
21+
syncAutoAnnotationToDatabaseUsingPost,
2122
} from "../annotation.api";
2223
import CreateAutoAnnotationDialog from "./components/CreateAutoAnnotationDialog";
2324
import EditAutoAnnotationDatasetDialog from "./components/EditAutoAnnotationDatasetDialog";
@@ -123,6 +124,32 @@ export default function AutoAnnotation() {
123124
setShowImportDialog(true);
124125
};
125126

127+
const handleSyncToDatabase = (task: AutoAnnotationTask) => {
128+
Modal.confirm({
129+
title: `确认将自动标注任务「${task.name}」在 Label Studio 中的标注结果同步到数据库吗?`,
130+
content: (
131+
<div>
132+
<div>此操作会根据 Label Studio 中的任务数据覆盖当前文件标签与标注信息。</div>
133+
<div>同步完成后,可在数据管理的文件详情中查看最新标签与标注。</div>
134+
</div>
135+
),
136+
okText: "同步到数据库",
137+
cancelText: "取消",
138+
onOk: async () => {
139+
const hide = message.loading("正在从 Label Studio 同步标注到数据库...", 0);
140+
try {
141+
await syncAutoAnnotationToDatabaseUsingPost(task.id);
142+
hide();
143+
message.success("同步完成");
144+
} catch (e) {
145+
console.error(e);
146+
hide();
147+
message.error("同步失败,请稍后重试");
148+
}
149+
},
150+
});
151+
};
152+
126153
const handleDelete = (task: AutoAnnotationTask) => {
127154
Modal.confirm({
128155
title: `确认删除自动标注任务「${task.name}」吗?`,
@@ -307,14 +334,14 @@ export default function AutoAnnotation() {
307334
编辑
308335
</Button>
309336
</Tooltip>
310-
<Tooltip title="从 Label Studio 同步标注结果到数据集">
337+
<Tooltip title="从 Label Studio 同步标注结果到数据库">
311338
<Button
312339
type="link"
313340
size="small"
314341
icon={<SyncOutlined />}
315-
onClick={() => handleImportFromLabelStudio(record)}
342+
onClick={() => handleSyncToDatabase(record)}
316343
>
317-
同步
344+
同步到数据库
318345
</Button>
319346
</Tooltip>
320347

@@ -344,6 +371,12 @@ export default function AutoAnnotation() {
344371
<Dropdown
345372
menu={{
346373
items: [
374+
{
375+
key: "export-result",
376+
label: "导出标注结果",
377+
icon: <DownloadOutlined />,
378+
onClick: () => handleImportFromLabelStudio(record),
379+
},
347380
{
348381
key: "edit-dataset",
349382
label: "编辑任务数据集",

frontend/src/pages/DataAnnotation/Create/components/CreateAnnotationTaskDialog.tsx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export default function CreateAnnotationTask({
120120
const [selectedDataset, setSelectedDataset] = useState<Dataset | null>(null);
121121
const [imageFileCount, setImageFileCount] = useState(0);
122122
const [manualDatasetTypeFilter, setManualDatasetTypeFilter] = useState<DatasetType | undefined>(undefined);
123+
const [manualAllowedExtensions, setManualAllowedExtensions] = useState<string[] | undefined>(undefined);
123124

124125
useEffect(() => {
125126
if (!open) return;
@@ -178,6 +179,11 @@ export default function CreateAnnotationTask({
178179
setImageFileCount(count);
179180
}, [selectedFilesMap]);
180181

182+
const IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tiff", ".webp"];
183+
const TEXT_EXTENSIONS = [".txt", ".md", ".csv", ".tsv", ".jsonl", ".log"];
184+
const AUDIO_EXTENSIONS = [".wav", ".mp3", ".flac", ".aac", ".ogg", ".m4a", ".wma"];
185+
const VIDEO_EXTENSIONS = [".mp4", ".avi", ".mov", ".mkv", ".flv", ".wmv", ".webm"];
186+
181187
const mapTemplateDataTypeToDatasetType = (raw?: string): DatasetType | undefined => {
182188
if (!raw) return undefined;
183189
const v = String(raw).trim().toLowerCase();
@@ -218,6 +224,40 @@ export default function CreateAnnotationTask({
218224
return undefined;
219225
};
220226

227+
const getAllowedExtensionsForTemplateDataType = (raw?: string): string[] | undefined => {
228+
if (!raw) return undefined;
229+
const v = String(raw).trim().toLowerCase();
230+
231+
const textTokens = new Set<string>([
232+
"text",
233+
DataType.TEXT.toLowerCase(),
234+
"文本",
235+
]);
236+
const imageTokens = new Set<string>([
237+
"image",
238+
DataType.IMAGE.toLowerCase(),
239+
"图像",
240+
"图片",
241+
]);
242+
const audioTokens = new Set<string>([
243+
"audio",
244+
DataType.AUDIO.toLowerCase(),
245+
"音频",
246+
]);
247+
const videoTokens = new Set<string>([
248+
"video",
249+
DataType.VIDEO.toLowerCase(),
250+
"视频",
251+
]);
252+
253+
if (textTokens.has(v)) return TEXT_EXTENSIONS;
254+
if (imageTokens.has(v)) return IMAGE_EXTENSIONS;
255+
if (audioTokens.has(v)) return AUDIO_EXTENSIONS;
256+
if (videoTokens.has(v)) return VIDEO_EXTENSIONS;
257+
258+
return undefined;
259+
};
260+
221261
const handleManualSubmit = async () => {
222262
try {
223263
const values = await manualForm.validateFields();
@@ -417,6 +457,9 @@ export default function CreateAnnotationTask({
417457
const nextType = mapTemplateDataTypeToDatasetType(tpl?.dataType);
418458
setManualDatasetTypeFilter(nextType);
419459

460+
const nextExtensions = getAllowedExtensionsForTemplateDataType(tpl?.dataType);
461+
setManualAllowedExtensions(nextExtensions);
462+
420463
// 若当前已选数据集类型与模板不匹配,则清空当前选择
421464
if (selectedDataset && nextType && selectedDataset.datasetType !== nextType) {
422465
setSelectedDataset(null);
@@ -459,6 +502,7 @@ export default function CreateAnnotationTask({
459502
}
460503
}}
461504
datasetTypeFilter={manualDatasetTypeFilter}
505+
allowedFileExtensions={manualAllowedExtensions}
462506
singleDatasetOnly
463507
disabled={!manualForm.getFieldValue("templateId")}
464508
/>
@@ -512,6 +556,7 @@ export default function CreateAnnotationTask({
512556
autoForm.setFieldsValue({ datasetId: dataset?.id ?? "" });
513557
}}
514558
datasetTypeFilter={DatasetType.IMAGE}
559+
allowedFileExtensions={IMAGE_EXTENSIONS}
515560
singleDatasetOnly
516561
/>
517562
{selectedDataset && (

0 commit comments

Comments
 (0)