From 977a930c9740a1195e8d40fed1d1feb723dde998 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Mon, 26 Jan 2026 23:54:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(annotation):=20=E6=94=AF=E6=8C=81=E9=9F=B3?= =?UTF-8?q?=E9=A2=91=E5=92=8C=E8=A7=86=E9=A2=91=E6=95=B0=E6=8D=AE=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E7=9A=84=E6=A0=87=E6=B3=A8=E4=BB=BB=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加了音频和视频数据类型常量定义 - 实现了音频和视频标注模板的内置配置 - 扩展前端组件以支持按数据类型过滤标注模板 - 重构后端编辑器服务以处理音频和视频任务构建 - 更新数据库初始化脚本包含音频和视频标注模板 - 添加音频和视频数据类型的预览URL映射逻辑 --- .../DataAnnotation/Create/CreateTask.tsx | 64 ++++- .../components/CreateAnnotationTaskDialog.tsx | 95 ++++++-- .../annotation/service/builtin_templates.py | 219 ++++++++++++++++++ .../app/module/annotation/service/editor.py | 97 ++++++-- scripts/db/data-annotation-init.sql | 45 ++-- 5 files changed, 461 insertions(+), 59 deletions(-) diff --git a/frontend/src/pages/DataAnnotation/Create/CreateTask.tsx b/frontend/src/pages/DataAnnotation/Create/CreateTask.tsx index 06f626d..81b5348 100644 --- a/frontend/src/pages/DataAnnotation/Create/CreateTask.tsx +++ b/frontend/src/pages/DataAnnotation/Create/CreateTask.tsx @@ -12,7 +12,7 @@ import { createAnnotationTaskUsingPost, queryAnnotationTemplatesUsingGet, } from "../annotation.api"; -import type { AnnotationTemplate } from "../annotation.model"; +import { DataType, type AnnotationTemplate } from "../annotation.model"; import TemplateConfigurationTreeEditor from "../components/TemplateConfigurationTreeEditor"; const DEFAULT_SEGMENTATION_ENABLED = true; @@ -20,6 +20,22 @@ const SEGMENTATION_OPTIONS = [ { label: "需要切片段", value: true }, { label: "不需要切片段", value: false }, ]; +const resolveTemplateDataType = (datasetType?: DatasetType) => { + switch (datasetType) { + case DatasetType.TEXT: + return DataType.TEXT; + case DatasetType.IMAGE: + return DataType.IMAGE; + case DatasetType.AUDIO: + return DataType.AUDIO; + case DatasetType.VIDEO: + return DataType.VIDEO; + default: + return undefined; + } +}; +const resolveDefaultTemplate = (items: AnnotationTemplate[]) => + items.find((template) => template.builtIn) || items[0]; export default function AnnotationTaskCreate() { const navigate = useNavigate(); @@ -48,9 +64,17 @@ export default function AnnotationTaskCreate() { } }; - const fetchTemplates = async () => { + const fetchTemplates = async (dataType?: string) => { + if (!dataType) { + setTemplates([]); + return; + } try { - const response = await queryAnnotationTemplatesUsingGet({ page: 1, size: 200 }); + const response = await queryAnnotationTemplatesUsingGet({ + page: 1, + size: 200, + dataType, + }); if (response.code === 200 && response.data) { setTemplates(response.data.content || []); } else { @@ -64,9 +88,39 @@ export default function AnnotationTaskCreate() { useEffect(() => { fetchDatasets(); - fetchTemplates(); }, []); + useEffect(() => { + if (!selectedDataset) { + setTemplates([]); + form.setFieldsValue({ templateId: undefined }); + setLabelConfig(""); + return; + } + const dataType = resolveTemplateDataType(selectedDataset.datasetType); + fetchTemplates(dataType); + }, [form, selectedDataset]); + + useEffect(() => { + if (configMode !== "template" || !selectedDataset) { + return; + } + if (templates.length === 0) { + form.setFieldsValue({ templateId: undefined }); + setLabelConfig(""); + return; + } + const currentTemplateId = form.getFieldValue("templateId"); + const currentTemplate = templates.find((template) => template.id === currentTemplateId); + if (currentTemplate) { + return; + } + const defaultTemplate = resolveDefaultTemplate(templates); + if (defaultTemplate) { + form.setFieldsValue({ templateId: defaultTemplate.id }); + setLabelConfig(defaultTemplate.labelConfig || ""); + } + }, [configMode, form, selectedDataset, templates]); const handleTemplateSelect = (value?: string) => { if (!value) { setLabelConfig(""); @@ -171,6 +225,8 @@ export default function AnnotationTaskCreate() { }))} onChange={(value) => { setSelectedDatasetId(value); + form.setFieldsValue({ templateId: undefined }); + setLabelConfig(""); const dataset = datasets.find((item) => item.id === value); if (dataset?.datasetType === DatasetType.TEXT) { const currentValue = form.getFieldValue("segmentationEnabled"); diff --git a/frontend/src/pages/DataAnnotation/Create/components/CreateAnnotationTaskDialog.tsx b/frontend/src/pages/DataAnnotation/Create/components/CreateAnnotationTaskDialog.tsx index 2498617..d993971 100644 --- a/frontend/src/pages/DataAnnotation/Create/components/CreateAnnotationTaskDialog.tsx +++ b/frontend/src/pages/DataAnnotation/Create/components/CreateAnnotationTaskDialog.tsx @@ -13,7 +13,7 @@ import { queryAnnotationTemplatesUsingGet, } from "../../annotation.api"; import { DatasetType, type Dataset } from "@/pages/DataManagement/dataset.model"; -import type { AnnotationTemplate, AnnotationTask } from "../../annotation.model"; +import { DataType, type AnnotationTemplate, type AnnotationTask } from "../../annotation.model"; import LabelStudioEmbed from "@/components/business/LabelStudioEmbed"; import TemplateConfigurationTreeEditor from "../../components/TemplateConfigurationTreeEditor"; import { useTagConfig } from "@/hooks/useTagConfig"; @@ -57,6 +57,22 @@ const SEGMENTATION_OPTIONS = [ { label: "需要切片段", value: true }, { label: "不需要切片段", value: false }, ]; +const resolveTemplateDataType = (datasetType?: DatasetType) => { + switch (datasetType) { + case DatasetType.TEXT: + return DataType.TEXT; + case DatasetType.IMAGE: + return DataType.IMAGE; + case DatasetType.AUDIO: + return DataType.AUDIO; + case DatasetType.VIDEO: + return DataType.VIDEO; + default: + return undefined; + } +}; +const resolveDefaultTemplate = (items: AnnotationTemplate[]) => + items.find((template) => template.builtIn) || items[0]; export default function CreateAnnotationTask({ open, @@ -112,19 +128,6 @@ export default function CreateAnnotationTask({ }); setDatasets(datasetData.content.map(mapDataset) || []); - // Fetch templates - const templateResponse = await queryAnnotationTemplatesUsingGet({ - page: 1, - size: 100, - }); - - if (templateResponse.code === 200 && templateResponse.data) { - const fetchedTemplates = templateResponse.data.content || []; - setTemplates(fetchedTemplates); - } else { - console.error("Failed to fetch templates:", templateResponse); - setTemplates([]); - } } catch (error) { console.error("Error fetching data:", error); setTemplates([]); @@ -133,6 +136,66 @@ export default function CreateAnnotationTask({ fetchData(); }, [open]); + const fetchTemplates = async (dataType?: string) => { + if (!dataType) { + setTemplates([]); + return; + } + try { + const templateResponse = await queryAnnotationTemplatesUsingGet({ + page: 1, + size: 200, + dataType, + }); + + if (templateResponse.code === 200 && templateResponse.data) { + const fetchedTemplates = templateResponse.data.content || []; + setTemplates(fetchedTemplates); + } else { + console.error("Failed to fetch templates:", templateResponse); + setTemplates([]); + } + } catch (error) { + console.error("Error fetching templates:", error); + setTemplates([]); + } + }; + + useEffect(() => { + if (!open || isEditMode) { + return; + } + if (!selectedDataset) { + setTemplates([]); + manualForm.setFieldsValue({ templateId: undefined }); + setLabelConfig(""); + return; + } + const dataType = resolveTemplateDataType(selectedDataset.datasetType); + fetchTemplates(dataType); + }, [isEditMode, manualForm, open, selectedDataset]); + + useEffect(() => { + if (!open || isEditMode || configMode !== "template" || !selectedDataset) { + return; + } + if (templates.length === 0) { + manualForm.setFieldsValue({ templateId: undefined }); + setLabelConfig(""); + return; + } + const currentTemplateId = manualForm.getFieldValue("templateId"); + const currentTemplate = templates.find((template) => template.id === currentTemplateId); + if (currentTemplate) { + return; + } + const defaultTemplate = resolveDefaultTemplate(templates); + if (defaultTemplate) { + manualForm.setFieldsValue({ templateId: defaultTemplate.id }); + setLabelConfig(defaultTemplate.labelConfig || ""); + } + }, [configMode, isEditMode, manualForm, open, selectedDataset, templates]); + // Reset form and manual-edit flag when modal opens, or load task data in edit mode useEffect(() => { if (open) { @@ -587,6 +650,10 @@ export default function CreateAnnotationTask({ })} onChange={(value) => { setSelectedDatasetId(value); + if (!isEditMode) { + manualForm.setFieldsValue({ templateId: undefined }); + setLabelConfig(""); + } const dataset = datasets.find((item) => item.id === value); if (dataset?.datasetType === DatasetType.TEXT) { const currentValue = manualForm.getFieldValue("segmentationEnabled"); diff --git a/runtime/datamate-python/app/module/annotation/service/builtin_templates.py b/runtime/datamate-python/app/module/annotation/service/builtin_templates.py index 7c9ec8b..02d994f 100644 --- a/runtime/datamate-python/app/module/annotation/service/builtin_templates.py +++ b/runtime/datamate-python/app/module/annotation/service/builtin_templates.py @@ -13,7 +13,11 @@ from app.module.annotation.utils.config_validator import LabelStudioConfigValida logger = get_logger(__name__) DATA_TYPE_IMAGE = "image" +DATA_TYPE_AUDIO = "audio" +DATA_TYPE_VIDEO = "video" CATEGORY_COMPUTER_VISION = "computer-vision" +CATEGORY_AUDIO_SPEECH = "audio-speech" +CATEGORY_VIDEO = "video" STYLE_HORIZONTAL = "horizontal" VERSION_DEFAULT = "1.0.0" @@ -51,6 +55,105 @@ SEMANTIC_SEGMENTATION_POLYGON_LABEL_CONFIG = """ """ +ASR_SEGMENTS_LABEL_CONFIG = """ + + +