lingmin_package@163.com 1 месяц назад
Родитель
Сommit
d5d038927b

+ 1 - 1
src/app/schemas/base.py

@@ -18,7 +18,7 @@ class BaseSchema(BaseModel):
 
 class ResponseSchema(BaseSchema):
     """统一响应Schema"""
-    code: int = Field(default=0, description="响应码")
+    code: str = Field(default="000000", description="响应码")
     message: str = Field(default="success", description="响应消息")
     data: Optional[Any] = Field(default=None, description="响应数据")
     timestamp: datetime = Field(default_factory=datetime.utcnow, description="响应时间")

+ 4 - 4
src/app/server/app.py

@@ -248,7 +248,7 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE
     return JSONResponse(
         status_code=status.HTTP_400_BAD_REQUEST,
         content={
-            "code": 100001,
+            "code": "100001",
             "message": "参数验证失败",
             "data": {"errors": errors},
             "timestamp": datetime.now(timezone.utc).isoformat()
@@ -264,7 +264,7 @@ async def general_exception_handler(request: Request, exc: Exception):
     return JSONResponse(
         status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
         content={
-            "code": 500001,
+            "code": "500001",
             "message": "服务器内部错误" if not config_handler.get_bool("admin_app", "DEBUG", False) else str(exc),
             "data": None,
             "timestamp": datetime.now(timezone.utc).isoformat()
@@ -277,7 +277,7 @@ async def general_exception_handler(request: Request, exc: Exception):
 async def health_check():
     """健康检查"""
     return ResponseSchema(
-        code=0,
+        code="000000",
         message="服务正常运行",
         data={
             "status": "healthy",
@@ -292,7 +292,7 @@ async def health_check():
 async def root():
     """根路径"""
     return ResponseSchema(
-        code=0,
+        code="000000",
         message="欢迎使用 LQAdminPlatform",
         data={
             "name": config_handler.get("admin_app", "APP_NAME", "后台管理"),

+ 81 - 11
src/app/services/task_service.py

@@ -1,4 +1,5 @@
 
+
 import logging
 import json
 import httpx
@@ -9,6 +10,43 @@ from app.base.minio_connection import get_minio_manager
 
 logger = logging.getLogger(__name__)
 
+
+def generate_tag_colors(tags: List[str]) -> Dict[str, str]:
+    """
+    为标签生成不重复的颜色
+
+    Args:
+        tags: 标签列表
+
+    Returns:
+        标签到颜色值的映射字典
+    """
+    # 预定义的一组高对比度、视觉区分度高的颜色
+    pre_defined_colors = [
+        "#FF5733", "#33FF57", "#3357FF", "#FF33A1", "#33FFF5",
+        "#F5FF33", "#FF8C33", "#8C33FF", "#FF3333", "#33FF8C",
+        "#FF338C", "#8CFF33", "#338CFF", "#FF5733", "#57FF33",
+        "#3357FF", "#FF33FF", "#33FFFF", "#FFFF33", "#FF8000",
+        "#80FF00", "#0080FF", "#FF0080", "#00FF80", "#8000FF",
+        "#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF",
+        "#00FFFF", "#FFA500", "#A52A2A", "#800080", "#008080",
+        "#000080", "#800000", "#008000", "#000000"
+    ]
+
+    color_map = {}
+    for i, tag in enumerate(tags):
+        # 使用预定义颜色,如果标签数量超过预定义颜色,则通过算法生成
+        if i < len(pre_defined_colors):
+            color_map[tag] = pre_defined_colors[i]
+        else:
+            # 使用 HSL 色相环生成颜色,确保颜色不重复
+            hue = (i * 137.508) % 360  # 黄金角度,确保颜色分布均匀
+            # 高饱和度和亮度,保证颜色鲜艳
+            color_map[tag] = f"hsl({hue:.0f}, 70%, 50%)"
+
+    return color_map
+
+
 class TaskService:
     """任务管理服务类"""
     
@@ -514,9 +552,11 @@ class TaskService:
                         img_url = self.minio_manager.get_full_url(img_url)
                     image_data_map[img_row['id']] = [img_url] if img_url else []
 
+            # 第一阶段:收集所有任务数据(不含颜色)
+            tasks_data = []
             for item in rows:
                 task_id = item['id']
-                
+
                 # 提取并处理标签
                 doc_tags = []
                 if item.get('tag'):
@@ -525,7 +565,7 @@ class TaskService:
                         if doc_tags:
                             for t in doc_tags: all_project_tags.add(t)
                     except: pass
-                
+
                 # 解析数据库元数据 (提前序列化日期)
                 db_metadata = {}
                 if item.get('metadata'):
@@ -534,7 +574,7 @@ class TaskService:
                         if db_metadata:
                             db_metadata = self._serialize_datetime(db_metadata)
                     except: pass
-                
+
                 # 获取任务内容
                 task_contents = []
                 if internal_task_type == 'data':
@@ -545,10 +585,27 @@ class TaskService:
                 elif internal_task_type == 'image':
                     task_contents = image_data_map.get(task_id, [])
 
-                # 构建最终任务列表
+                # 保存任务数据(不含标签颜色)
+                tasks_data.append({
+                    'task_id': task_id,
+                    'doc_tags': doc_tags,
+                    'db_metadata': db_metadata,
+                    'task_contents': task_contents
+                })
+
+            # 第二阶段:生成颜色并构建最终任务列表
+            sorted_tags = sorted(list(all_project_tags))
+            tag_color_map = generate_tag_colors(sorted_tags)
+
+            for task_data in tasks_data:
+                task_id = task_data['task_id']
+                doc_tags = task_data['doc_tags']
+                db_metadata = task_data['db_metadata']
+                task_contents = task_data['task_contents']
+
                 for idx, content in enumerate(task_contents):
                     if not content: continue
-                    
+
                     # 合并元数据:数据库数据 + 动态 ID
                     task_metadata = {
                         "original_id": task_id,
@@ -556,22 +613,34 @@ class TaskService:
                     }
                     if db_metadata:
                         task_metadata.update(db_metadata)
-                    
+
+                    # 为每个标签添加颜色
                     if doc_tags:
-                        task_metadata['tags'] = [{"tag": tag} for tag in doc_tags]
-                        
+                        task_metadata['tags'] = [
+                            {"tag": tag, "color": tag_color_map.get(tag, "#999999")}
+                            for tag in doc_tags
+                        ]
+
                     task_item = {
                         "id": f"{task_id}_{idx}" if len(task_contents) > 1 else task_id,
                         "content": content,
                         "metadata": task_metadata
                     }
-                    
+
                     # 尝试从元数据中提取 annotation_result
                     if db_metadata and 'annotation_result' in db_metadata:
                         task_item['annotation_result'] = db_metadata['annotation_result']
-                    
+
                     final_tasks.append(task_item)
 
+            # 构建项目级别的标签列表(带颜色)
+            tags_result = []
+            for tag in sorted_tags:
+                tag_obj = {"tag": tag}
+                if tag in tag_color_map:
+                    tag_obj["color"] = tag_color_map[tag]
+                tags_result.append(tag_obj)
+
             # 准备返回结果,不再进行全局递归序列化 (已在局部处理)
             return {
                 "name": project_name,
@@ -579,7 +648,7 @@ class TaskService:
                 "task_type": external_task_type,
                 "data": final_tasks,
                 "external_id": remote_project_id,
-                "tags": [{"tag": t} for t in sorted(list(all_project_tags))]
+                "tags": tags_result
             }
         except Exception as e:
             logger.exception(f"导出项目数据异常: {e}")
@@ -956,6 +1025,7 @@ class TaskService:
                     "Content-Type": "application/json"
                 }
                 logger.info(f"正在推送项目 {project_id} 至外部平台: {api_url}, 数据条数: {len(payload['data'])}")
+                logger.info(f"正在推送项目: {payload['data']}")
                 response = await client.post(api_url, json=payload, headers=headers)
                 
                 if response.status_code in (200, 201):

+ 5 - 5
src/views/auth_view.py

@@ -81,7 +81,7 @@ async def login(
     except AuthenticationError as e:
         logger.warning(f"认证失败: username={login_data.username}, reason={e.message}")
         return ResponseSchema(
-            code=e.code,
+            code=str(e.code),
             message=e.message,
             data=None
         )
@@ -112,10 +112,10 @@ async def refresh_token(
             message="令牌刷新成功",
             data=token_response.dict()
         )
-        
+
     except AuthenticationError as e:
         return ResponseSchema(
-            code=e.code,
+            code=str(e.code),
             message=e.message,
             data=None
         )
@@ -199,10 +199,10 @@ async def get_user_info(
             return response_dict
         
         return response_data
-        
+
     except AuthenticationError as e:
         return ResponseSchema(
-            code=e.code,
+            code=str(e.code),
             message=e.message,
             data=None
         )

+ 3 - 3
src/views/image_view.py

@@ -184,7 +184,7 @@ async def batch_add_to_task(req: BatchAddRequest, current_user: dict = Depends(g
         success, message = await service.batch_add_to_task(req.ids, username, req.project_name, tags=req.tags)
         
         return ApiResponse(
-            code=0 if success else 500, 
+            code="000000" if success else "500", 
             message=message, 
             timestamp=datetime.now(timezone.utc).isoformat()
         ).model_dump()
@@ -205,7 +205,7 @@ async def category_batch_add_to_task(req: CategoryBatchAddRequest, current_user:
         )
         
         return ApiResponse(
-            code=0 if success else 500, 
+            code="000000" if success else 500, 
             message=message, 
             data=data,
             timestamp=datetime.now(timezone.utc).isoformat()
@@ -222,7 +222,7 @@ async def category_batch_check(category_id: str = Query("", description="分类I
         success, message, data = await service.category_batch_check(category_id)
         
         return ApiResponse(
-            code=0 if success else 500, 
+            code="000000" if success else 500, 
             message=message, 
             data=data,
             timestamp=datetime.now(timezone.utc).isoformat()

+ 106 - 18
项目/API接口定义/标注-样本中心联动接口.md

@@ -69,7 +69,12 @@ POST /api/external/projects/init
       }
     }
   ],
-  "external_id": "sample_center_proj_001"
+  "external_id": "sample_center_proj_001",
+  "tags": [
+    {"tag": "猫猫", "color": "#FF5733"},
+    {"tag": "狗狗", "color": "#33FF57"},
+    {"tag": "其他"}
+  ]
 }
 ```
 
@@ -85,8 +90,13 @@ POST /api/external/projects/init
 | data[].content | string | 是 | 数据内容(文本或图像URL) |
 | data[].metadata | object | 否 | 额外元数据 |
 | external_id | string | 否 | 外部系统的项目ID,用于关联查询 |
+| tags | array | 否 | 标签列表,用于预定义标注选项 |
+| tags[].tag | string | 是 | 标签名称 |
+| tags[].color | string | 否 | 标签颜色,格式: #RRGGBB,不传则自动生成 |
 
-**注意**:标签(labels)由标注平台管理员在项目配置阶段设置,样本中心无需提供。
+**注意**:
+- 如果传入 `tags` 参数,系统会自动根据标签生成对应的XML配置
+- 如果不传 `tags` 参数,标签由标注平台管理员在项目配置阶段设置
 
 **支持的任务类型**
 
@@ -94,8 +104,9 @@ POST /api/external/projects/init
 |-----------|------|-------------------|
 | text_classification | 文本分类 | 文本内容 |
 | image_classification | 图像分类 | 图像URL |
-| object_detection | 目标检测 | 图像URL |
+| object_detection | 目标检测(矩形框) | 图像URL |
 | ner | 命名实体识别 | 文本内容 |
+| polygon | 多边形标注 | 图像URL |
 
 **响应**
 
@@ -254,6 +265,7 @@ POST /api/external/projects/{project_id}/export
 | yolo | YOLO目标检测格式 | YOLO模型训练 |
 | coco | COCO数据集格式 | 目标检测/分割模型训练 |
 | alpaca | Alpaca指令微调格式 | LLM指令微调 |
+| pascal_voc | PascalVOC XML格式 | 经典目标检测模型训练 |
 
 **响应**
 
@@ -340,6 +352,23 @@ POST /api/external/projects/{project_id}/export
 ]
 ```
 
+**PascalVOC格式**:
+```json
+[
+  {
+    "image": "https://example.com/img1.jpg",
+    "filename": "img1.jpg",
+    "xml_content": "<?xml version=\"1.0\"?>...",
+    "objects": [
+      {
+        "name": "cat",
+        "bndbox": {"xmin": 100, "ymin": 50, "xmax": 300, "ymax": 250}
+      }
+    ]
+  }
+]
+```
+
 ---
 
 ## 错误响应
@@ -385,7 +414,7 @@ headers = {
     "Content-Type": "application/json"
 }
 
-# 1. 创建项目
+# 1. 创建项目(带标签)
 init_data = {
     "name": "文本分类项目",
     "description": "对用户评论进行情感分类",
@@ -395,7 +424,12 @@ init_data = {
         {"id": "2", "content": "质量太差了,不推荐"},
         {"id": "3", "content": "一般般,没什么特别的"}
     ],
-    "external_id": "sample_center_001"
+    "external_id": "sample_center_001",
+    "tags": [
+        {"tag": "正面", "color": "#4CAF50"},
+        {"tag": "负面", "color": "#F44336"},
+        {"tag": "中性"}  # 不指定颜色,系统自动生成
+    ]
 }
 
 response = requests.post(
@@ -407,7 +441,30 @@ project = response.json()
 project_id = project["project_id"]
 print(f"项目创建成功: {project_id}")
 
-# 2. 查询进度
+# 2. 创建多边形标注项目
+polygon_data = {
+    "name": "多边形标注项目",
+    "description": "标注图像中的不规则区域",
+    "task_type": "polygon",
+    "data": [
+        {"id": "1", "content": "https://example.com/img1.jpg"},
+        {"id": "2", "content": "https://example.com/img2.jpg"}
+    ],
+    "tags": [
+        {"tag": "区域A", "color": "#FF5722"},
+        {"tag": "区域B", "color": "#607D8B"}
+    ]
+}
+
+response = requests.post(
+    f"{BASE_URL}/projects/init",
+    json=polygon_data,
+    headers=headers
+)
+polygon_project = response.json()
+print(f"多边形项目创建成功: {polygon_project['project_id']}")
+
+# 3. 查询进度
 response = requests.get(
     f"{BASE_URL}/projects/{project_id}/progress",
     headers=headers
@@ -415,7 +472,7 @@ response = requests.get(
 progress = response.json()
 print(f"完成进度: {progress['completion_percentage']}%")
 
-# 3. 导出数据(ShareGPT格式)
+# 4. 导出数据(ShareGPT格式)
 export_data = {
     "format": "sharegpt",
     "completed_only": True,
@@ -444,7 +501,7 @@ print(f"文件已保存: {export_result['file_name']}")
 ### cURL 示例
 
 ```bash
-# 1. 创建项目
+# 1. 创建项目(带标签)
 curl -X POST "http://localhost:8003/api/external/projects/init" \
   -H "Authorization: Bearer your_admin_token" \
   -H "Content-Type: application/json" \
@@ -455,20 +512,40 @@ curl -X POST "http://localhost:8003/api/external/projects/init" \
       {"id": "1", "content": "https://example.com/img1.jpg"},
       {"id": "2", "content": "https://example.com/img2.jpg"}
     ],
-    "external_id": "sample_center_proj_001"
+    "external_id": "sample_center_proj_001",
+    "tags": [
+      {"tag": "猫猫", "color": "#FF9800"},
+      {"tag": "狗狗", "color": "#2196F3"}
+    ]
+  }'
+
+# 2. 创建多边形标注项目
+curl -X POST "http://localhost:8003/api/external/projects/init" \
+  -H "Authorization: Bearer your_admin_token" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "name": "多边形标注项目",
+    "task_type": "polygon",
+    "data": [
+      {"id": "1", "content": "https://example.com/img1.jpg"}
+    ],
+    "tags": [
+      {"tag": "区域A", "color": "#FF5722"},
+      {"tag": "区域B"}
+    ]
   }'
 
-# 2. 查询进度
+# 3. 查询进度
 curl -X GET "http://localhost:8003/api/external/projects/proj_xxx/progress" \
   -H "Authorization: Bearer your_admin_token"
 
-# 3. 导出数据(YOLO格式)
+# 4. 导出数据(YOLO格式)
 curl -X POST "http://localhost:8003/api/external/projects/proj_xxx/export" \
   -H "Authorization: Bearer your_admin_token" \
   -H "Content-Type: application/json" \
   -d '{"format": "yolo", "completed_only": true}'
 
-# 4. 下载导出文件
+# 5. 下载导出文件
 curl -X GET "http://localhost:8003/api/exports/export_xxx/download" \
   -H "Authorization: Bearer your_admin_token" \
   -o exported_data.zip
@@ -512,17 +589,26 @@ curl -X GET "http://localhost:8003/api/exports/export_xxx/download" \
 
 1. **项目状态**:外部系统创建的项目初始状态为 `draft`,需要标注平台管理员完成配置(设置标签等)和任务分发后才会进入 `in_progress` 状态。
 
-2. **标签配置**:标签由标注平台管理员在项目配置阶段设置,样本中心只需提供任务数据,无需关心标签定义。
+2. **标签配置**:
+   - 如果在创建项目时传入 `tags` 参数,系统会自动生成包含这些标签的XML配置
+   - 如果不传 `tags` 参数,标签由标注平台管理员在项目配置阶段设置
+   - 管理员可以在配置阶段修改已传入的标签名称和颜色
 
-3. **数据格式**:
+3. **颜色格式**:
+   - 颜色使用 `#RRGGBB` 格式(如 `#FF5733`)
+   - 如果不指定颜色,系统会自动生成随机颜色
+
+4. **数据格式**:
    - 文本类任务(text_classification, ner):`content` 字段为文本内容
-   - 图像类任务(image_classification, object_detection):`content` 字段为图像URL
+   - 图像类任务(image_classification, object_detection, polygon):`content` 字段为图像URL
+
+5. **外部ID关联**:建议在创建项目时提供 `external_id`,方便后续在样本中心系统中关联查询。
 
-4. **外部ID关联**:建议在创建项目时提供 `external_id`,方便后续在样本中心系统中关联查询。
+6. **导出时机**:建议在项目状态为 `completed` 或 `completion_percentage` 达到预期值时再进行数据导出
 
-5. **导出时机**:建议在项目状态为 `completed` 或 `completion_percentage` 达到预期值时再进行数据导出
+7. **Token安全**:请妥善保管管理员Token,不要在客户端代码中暴露
 
-6. **Token安全**:请妥善保管管理员Token,不要在客户端代码中暴露
+8. **多边形标注**:polygon 类型支持标注不规则形状区域,导出时支持 COCO 和 YOLO 格式
 
 ---
 
@@ -530,4 +616,6 @@ curl -X GET "http://localhost:8003/api/exports/export_xxx/download" \
 
 | 版本 | 日期 | 说明 |
 |------|------|------|
+| 1.2.0 | 2026-02-06 | 添加PascalVOC导出格式支持 |
+| 1.1.0 | 2026-02-05 | 添加tags参数支持、新增polygon任务类型 |
 | 1.0.0 | 2026-02-03 | 初始版本 |