You've already forked DataMate
refactor(annotation): 移除对 Label Studio Server 的依赖并切换到内嵌编辑器模式
- 移除 LabelStudioClient 和 SyncService 的导入及使用 - 删除与 Label Studio 项目的创建、删除和同步相关代码 - 修改创建数据集映射功能,改为创建 DataMate 标注项目 - 更新删除映射接口,仅进行软删除不再删除 Label Studio 项目 - 修改同步接口为兼容性保留,实际操作为空操作 - 移除 Label Studio 连接诊断功能 - 更新文档说明以反映内嵌编辑器模式的变化
This commit is contained in:
@@ -11,7 +11,6 @@ npm run mock # 启动后台Mock服务(可选)
|
|||||||
```
|
```
|
||||||
frontend/
|
frontend/
|
||||||
├── public/ # 📖 文档中心
|
├── public/ # 📖 文档中心
|
||||||
│ ├── huawei-logo.webp/ # logo
|
|
||||||
│ └── xxx/ # 标注工作台(可分离部署)
|
│ └── xxx/ # 标注工作台(可分离部署)
|
||||||
│
|
│
|
||||||
├── src/ # 🎨 前端应用
|
├── src/ # 🎨 前端应用
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/huawei-logo.webp" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>DataMate</title>
|
<title>DataMate</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -45,6 +45,19 @@
|
|||||||
window.parent.postMessage({ type, payload }, ORIGIN);
|
window.parent.postMessage({ type, payload }, ORIGIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.addEventListener("error", (event) => {
|
||||||
|
try {
|
||||||
|
postToParent("LS_ERROR", { message: event?.message || "iframe 内发生脚本错误" });
|
||||||
|
} catch (_) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("unhandledrejection", (event) => {
|
||||||
|
try {
|
||||||
|
const reason = event?.reason;
|
||||||
|
postToParent("LS_ERROR", { message: reason?.message || String(reason || "iframe 内发生未处理异常") });
|
||||||
|
} catch (_) {}
|
||||||
|
});
|
||||||
|
|
||||||
function destroyLabelStudio() {
|
function destroyLabelStudio() {
|
||||||
try {
|
try {
|
||||||
if (lsInstance && typeof lsInstance.destroy === "function") {
|
if (lsInstance && typeof lsInstance.destroy === "function") {
|
||||||
@@ -166,12 +179,17 @@
|
|||||||
: { id: selected?.id || "draft", result: (selected && selected.result) || [] };
|
: { id: selected?.id || "draft", result: (selected && selected.result) || [] };
|
||||||
|
|
||||||
// 最小化对齐 Label Studio Server 的字段(DataMate 侧会原样存储)
|
// 最小化对齐 Label Studio Server 的字段(DataMate 侧会原样存储)
|
||||||
if (!annotationPayload.task) annotationPayload.task = currentTask?.id || null;
|
const taskId = typeof currentTask?.id === "number" ? currentTask.id : Number(currentTask?.id) || null;
|
||||||
|
const fileId = currentTask?.data?.file_id || currentTask?.data?.fileId || null;
|
||||||
|
|
||||||
|
annotationPayload.id = typeof annotationPayload.id === "number" ? annotationPayload.id : taskId || 1;
|
||||||
|
annotationPayload.task = taskId;
|
||||||
if (!annotationPayload.created_at) annotationPayload.created_at = new Date().toISOString();
|
if (!annotationPayload.created_at) annotationPayload.created_at = new Date().toISOString();
|
||||||
annotationPayload.updated_at = new Date().toISOString();
|
annotationPayload.updated_at = new Date().toISOString();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
taskId: currentTask?.id || null,
|
taskId,
|
||||||
|
fileId,
|
||||||
annotation: annotationPayload,
|
annotation: annotationPayload,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,16 +145,16 @@ export default function LabelStudioTextEditor() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const saveFromExport = async (payload: any) => {
|
const saveFromExport = async (payload: any) => {
|
||||||
const taskId = payload?.taskId;
|
const fileId = payload?.fileId;
|
||||||
const annotation = payload?.annotation;
|
const annotation = payload?.annotation;
|
||||||
if (!taskId || !annotation) {
|
if (!fileId || !annotation) {
|
||||||
message.error("导出标注失败:缺少 taskId/annotation");
|
message.error("导出标注失败:缺少 fileId/annotation");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
await upsertEditorAnnotationUsingPut(projectId, String(taskId), { annotation });
|
await upsertEditorAnnotationUsingPut(projectId, String(fileId), { annotation });
|
||||||
message.success("标注已保存");
|
message.success("标注已保存");
|
||||||
await loadTasks(true);
|
await loadTasks(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -272,7 +272,7 @@ export default function LabelStudioTextEditor() {
|
|||||||
返回
|
返回
|
||||||
</Button>
|
</Button>
|
||||||
<Typography.Title level={4} style={{ margin: 0 }}>
|
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||||
标注(内嵌编辑器)
|
标注
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import uuid
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
import hashlib
|
||||||
import httpx
|
import httpx
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from sqlalchemy import func, select
|
from sqlalchemy import func, select
|
||||||
@@ -41,6 +42,27 @@ class AnnotationEditorService:
|
|||||||
self.db = db
|
self.db = db
|
||||||
self.template_service = AnnotationTemplateService()
|
self.template_service = AnnotationTemplateService()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _stable_ls_id(seed: str) -> int:
|
||||||
|
"""
|
||||||
|
生成稳定的 Label Studio 风格整数 ID(JS 安全整数范围内)。
|
||||||
|
|
||||||
|
说明:
|
||||||
|
- Label Studio Frontend 的 mobx-state-tree 模型对 task/annotation 的 id 有类型约束(通常为 number)。
|
||||||
|
- DataMate 使用 UUID 作为 file_id/project_id,因此需映射为整数供编辑器使用。
|
||||||
|
- 取 sha1 的前 13 个 hex(52bit),落在 JS Number 的安全整数范围。
|
||||||
|
"""
|
||||||
|
digest = hashlib.sha1(seed.encode("utf-8")).hexdigest()
|
||||||
|
value = int(digest[:13], 16)
|
||||||
|
return value if value > 0 else 1
|
||||||
|
|
||||||
|
def _make_ls_task_id(self, project_id: str, file_id: str) -> int:
|
||||||
|
return self._stable_ls_id(f"task:{project_id}:{file_id}")
|
||||||
|
|
||||||
|
def _make_ls_annotation_id(self, project_id: str, file_id: str) -> int:
|
||||||
|
# 单人单份最终标签:每个 task 只保留一个 annotation,id 直接与 task 绑定即可
|
||||||
|
return self._stable_ls_id(f"annotation:{project_id}:{file_id}")
|
||||||
|
|
||||||
async def _get_project_or_404(self, project_id: str) -> LabelingProject:
|
async def _get_project_or_404(self, project_id: str) -> LabelingProject:
|
||||||
result = await self.db.execute(
|
result = await self.db.execute(
|
||||||
select(LabelingProject).where(
|
select(LabelingProject).where(
|
||||||
@@ -207,8 +229,10 @@ class AnnotationEditorService:
|
|||||||
)
|
)
|
||||||
ann = ann_result.scalar_one_or_none()
|
ann = ann_result.scalar_one_or_none()
|
||||||
|
|
||||||
|
ls_task_id = self._make_ls_task_id(project_id, file_id)
|
||||||
|
|
||||||
task: Dict[str, Any] = {
|
task: Dict[str, Any] = {
|
||||||
"id": file_id,
|
"id": ls_task_id,
|
||||||
"data": {
|
"data": {
|
||||||
"text": text_content,
|
"text": text_content,
|
||||||
"file_id": file_id,
|
"file_id": file_id,
|
||||||
@@ -222,7 +246,11 @@ class AnnotationEditorService:
|
|||||||
if ann:
|
if ann:
|
||||||
annotation_updated_at = ann.updated_at
|
annotation_updated_at = ann.updated_at
|
||||||
# 直接返回存储的 annotation 原始对象(Label Studio 兼容)
|
# 直接返回存储的 annotation 原始对象(Label Studio 兼容)
|
||||||
task["annotations"] = [ann.annotation]
|
stored = dict(ann.annotation or {})
|
||||||
|
stored["task"] = ls_task_id
|
||||||
|
if not isinstance(stored.get("id"), int):
|
||||||
|
stored["id"] = self._make_ls_annotation_id(project_id, file_id)
|
||||||
|
task["annotations"] = [stored]
|
||||||
|
|
||||||
return EditorTaskResponse(
|
return EditorTaskResponse(
|
||||||
task=task,
|
task=task,
|
||||||
@@ -247,6 +275,11 @@ class AnnotationEditorService:
|
|||||||
if not isinstance(result, list):
|
if not isinstance(result, list):
|
||||||
raise HTTPException(status_code=400, detail="annotation.result 必须为数组")
|
raise HTTPException(status_code=400, detail="annotation.result 必须为数组")
|
||||||
|
|
||||||
|
ls_task_id = self._make_ls_task_id(project_id, file_id)
|
||||||
|
annotation_payload["task"] = ls_task_id
|
||||||
|
if not isinstance(annotation_payload.get("id"), int):
|
||||||
|
annotation_payload["id"] = self._make_ls_annotation_id(project_id, file_id)
|
||||||
|
|
||||||
existing_result = await self.db.execute(
|
existing_result = await self.db.execute(
|
||||||
select(AnnotationResult).where(
|
select(AnnotationResult).where(
|
||||||
AnnotationResult.project_id == project_id,
|
AnnotationResult.project_id == project_id,
|
||||||
@@ -262,8 +295,6 @@ class AnnotationEditorService:
|
|||||||
if existing.updated_at != request.expected_updated_at.replace(tzinfo=None):
|
if existing.updated_at != request.expected_updated_at.replace(tzinfo=None):
|
||||||
raise HTTPException(status_code=409, detail="标注已被更新,请刷新后重试")
|
raise HTTPException(status_code=409, detail="标注已被更新,请刷新后重试")
|
||||||
|
|
||||||
# 固定 annotation.id 为记录ID,保持稳定
|
|
||||||
annotation_payload["id"] = existing.id
|
|
||||||
existing.annotation = annotation_payload # type: ignore[assignment]
|
existing.annotation = annotation_payload # type: ignore[assignment]
|
||||||
existing.updated_at = now # type: ignore[assignment]
|
existing.updated_at = now # type: ignore[assignment]
|
||||||
await self.db.commit()
|
await self.db.commit()
|
||||||
@@ -275,7 +306,6 @@ class AnnotationEditorService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
new_id = str(uuid.uuid4())
|
new_id = str(uuid.uuid4())
|
||||||
annotation_payload["id"] = new_id
|
|
||||||
record = AnnotationResult(
|
record = AnnotationResult(
|
||||||
id=new_id,
|
id=new_id,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
|
|||||||
Reference in New Issue
Block a user