You've already forked DataMate
Add support for detecting new file versions and switching to them: Backend Changes: - Add file_version column to AnnotationResult model - Create Alembic migration for database schema update - Implement check_file_version() method to compare annotation and file versions - Implement use_new_version() method to clear annotations and update version - Update upsert_annotation() to record file version when saving - Add new API endpoints: GET /version and POST /use-new-version - Add FileVersionCheckResponse and UseNewVersionResponse schemas Frontend Changes: - Add checkFileVersionUsingGet and useNewVersionUsingPost API calls - Add version warning banner showing current vs latest file version - Add 'Use New Version' button with confirmation dialog - Clear version info state when switching files to avoid stale warnings Bug Fixes: - Fix previousFileVersion returning updated value (save before update) - Handle null file_version for historical data compatibility - Fix segmented annotation clearing (preserve structure, clear results) - Fix files without annotations incorrectly showing new version warnings - Preserve total_segments when clearing segmented annotations Files Modified: - frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx - frontend/src/pages/DataAnnotation/annotation.api.ts - runtime/datamate-python/app/db/models/annotation_management.py - runtime/datamate-python/app/module/annotation/interface/editor.py - runtime/datamate-python/app/module/annotation/schema/editor.py - runtime/datamate-python/app/module/annotation/service/editor.py New Files: - runtime/datamate-python/alembic.ini - runtime/datamate-python/alembic/env.py - runtime/datamate-python/alembic/script.py.mako - runtime/datamate-python/alembic/versions/20250205_0001_add_file_version.py
159 lines
5.5 KiB
Python
159 lines
5.5 KiB
Python
"""
|
|
Label Studio Editor(前端嵌入式)接口
|
|
|
|
说明:
|
|
- 不依赖 Label Studio Server;仅复用其“编辑器”前端库
|
|
- DataMate 负责提供 tasks/annotations 数据与保存能力
|
|
- 当前支持 dataset_type=TEXT/IMAGE 的项目
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Depends, Query, Path
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.logging import get_logger
|
|
from app.db.session import get_db
|
|
from app.module.annotation.schema.editor import (
|
|
EditorProjectInfo,
|
|
EditorTaskListResponse,
|
|
EditorTaskSegmentResponse,
|
|
EditorTaskResponse,
|
|
FileVersionCheckResponse,
|
|
UseNewVersionResponse,
|
|
UpsertAnnotationRequest,
|
|
UpsertAnnotationResponse,
|
|
)
|
|
from app.module.annotation.service.editor import AnnotationEditorService
|
|
from app.module.shared.schema import StandardResponse
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
router = APIRouter(
|
|
prefix="/editor",
|
|
tags=["annotation/editor"],
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/projects/{project_id}",
|
|
response_model=StandardResponse[EditorProjectInfo],
|
|
)
|
|
async def get_editor_project_info(
|
|
project_id: str = Path(..., description="标注项目ID(t_dm_labeling_projects.id)"),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
service = AnnotationEditorService(db)
|
|
info = await service.get_project_info(project_id)
|
|
return StandardResponse(code=200, message="success", data=info)
|
|
|
|
|
|
@router.get(
|
|
"/projects/{project_id}/tasks",
|
|
response_model=StandardResponse[EditorTaskListResponse],
|
|
)
|
|
async def list_editor_tasks(
|
|
project_id: str = Path(..., description="标注项目ID(t_dm_labeling_projects.id)"),
|
|
page: int = Query(0, ge=0, description="页码(从0开始)"),
|
|
size: int = Query(50, ge=1, le=200, description="每页大小"),
|
|
exclude_source_documents: Optional[bool] = Query(
|
|
None,
|
|
alias="excludeSourceDocuments",
|
|
description="是否排除已被转换为TXT的源文档文件(PDF/DOC/DOCX,仅文本数据集生效)",
|
|
),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
service = AnnotationEditorService(db)
|
|
result = await service.list_tasks(
|
|
project_id,
|
|
page=page,
|
|
size=size,
|
|
exclude_source_documents=exclude_source_documents,
|
|
)
|
|
return StandardResponse(code=200, message="success", data=result)
|
|
|
|
|
|
@router.get(
|
|
"/projects/{project_id}/tasks/{file_id}",
|
|
response_model=StandardResponse[EditorTaskResponse],
|
|
)
|
|
async def get_editor_task(
|
|
project_id: str = Path(..., description="标注项目ID(t_dm_labeling_projects.id)"),
|
|
file_id: str = Path(..., description="文件ID(t_dm_dataset_files.id)"),
|
|
segment_index: Optional[int] = Query(
|
|
None, alias="segmentIndex", description="段落索引(分段模式下使用)"
|
|
),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
service = AnnotationEditorService(db)
|
|
task = await service.get_task(project_id, file_id, segment_index=segment_index)
|
|
return StandardResponse(code=200, message="success", data=task)
|
|
|
|
|
|
@router.get(
|
|
"/projects/{project_id}/tasks/{file_id}/segments",
|
|
response_model=StandardResponse[EditorTaskSegmentResponse],
|
|
)
|
|
async def get_editor_task_segment(
|
|
project_id: str = Path(..., description="标注项目ID(t_dm_labeling_projects.id)"),
|
|
file_id: str = Path(..., description="文件ID(t_dm_dataset_files.id)"),
|
|
segment_index: int = Query(
|
|
..., ge=0, alias="segmentIndex", description="段落索引(从0开始)"
|
|
),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
service = AnnotationEditorService(db)
|
|
result = await service.get_task_segment(project_id, file_id, segment_index)
|
|
return StandardResponse(code=200, message="success", data=result)
|
|
|
|
|
|
@router.put(
|
|
"/projects/{project_id}/tasks/{file_id}/annotation",
|
|
response_model=StandardResponse[UpsertAnnotationResponse],
|
|
)
|
|
async def upsert_editor_annotation(
|
|
request: UpsertAnnotationRequest,
|
|
project_id: str = Path(..., description="标注项目ID(t_dm_labeling_projects.id)"),
|
|
file_id: str = Path(..., description="文件ID(t_dm_dataset_files.id)"),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
service = AnnotationEditorService(db)
|
|
result = await service.upsert_annotation(project_id, file_id, request)
|
|
return StandardResponse(code=200, message="success", data=result)
|
|
|
|
|
|
@router.get(
|
|
"/projects/{project_id}/files/{file_id}/version",
|
|
response_model=StandardResponse[FileVersionCheckResponse],
|
|
)
|
|
async def check_file_version(
|
|
project_id: str = Path(..., description="标注项目ID(t_dm_labeling_projects.id)"),
|
|
file_id: str = Path(..., description="文件ID(t_dm_dataset_files.id)"),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""
|
|
检查文件是否有新版本
|
|
"""
|
|
service = AnnotationEditorService(db)
|
|
result = await service.check_file_version(project_id, file_id)
|
|
return StandardResponse(code=200, message="success", data=result)
|
|
|
|
|
|
@router.post(
|
|
"/projects/{project_id}/files/{file_id}/use-new-version",
|
|
response_model=StandardResponse[UseNewVersionResponse],
|
|
)
|
|
async def use_new_version(
|
|
project_id: str = Path(..., description="标注项目ID(t_dm_labeling_projects.id)"),
|
|
file_id: str = Path(..., description="文件ID(t_dm_dataset_files.id)"),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""
|
|
使用文件新版本并清空标注
|
|
"""
|
|
service = AnnotationEditorService(db)
|
|
result = await service.use_new_version(project_id, file_id)
|
|
return StandardResponse(code=200, message="success", data=result)
|