本设计文档描述了标注平台的任务分配与模板管理功能的技术实现方案。该功能将完善现有平台的工作流程,实现从管理员(数据源)→ 任务派发 → 人员标注任务分配 → 数据收集 → 数据导出的完整闭环。
主要包含两大模块:
技术栈:
graph TB
subgraph "Frontend (web/apps/lq_label)"
A[React App] --> B[Layout Component]
B --> C[User Management View]
B --> D[Task Assignment View]
B --> E[Template Manager View]
B --> F[Config Editor View]
B --> G[Export View]
E --> H[Template Gallery]
E --> I[Template Preview]
F --> J[Monaco Editor]
F --> K[Visual Builder]
F --> L[Live Preview]
A --> M[Jotai State Management]
end
subgraph "Backend (backend/)"
N[FastAPI Server] --> O[User API]
N --> P[Task Assignment API]
N --> Q[Template API]
N --> R[Export API]
N --> S[Statistics API]
O --> T[Database]
P --> T
Q --> T
R --> T
S --> T
end
A -->|HTTP/REST| N
sequenceDiagram
participant Admin as 管理员
participant Frontend as 前端
participant Backend as 后端
participant DB as 数据库
Admin->>Frontend: 选择任务
Admin->>Frontend: 点击分配按钮
Frontend->>Backend: GET /api/users?role=annotator
Backend->>DB: 查询标注人员列表
DB-->>Backend: 返回用户列表
Backend-->>Frontend: 返回用户列表
Frontend->>Admin: 显示用户选择对话框
Admin->>Frontend: 选择用户并确认
Frontend->>Backend: PUT /api/tasks/{id}/assign
Backend->>DB: 更新任务分配
DB-->>Backend: 确认更新
Backend-->>Frontend: 返回更新后的任务
Frontend->>Admin: 显示分配成功
sequenceDiagram
participant Admin as 管理员
participant Frontend as 前端
participant Editor as 编辑器
Admin->>Frontend: 创建项目
Frontend->>Admin: 显示模板选择界面
Admin->>Frontend: 选择模板类别
Frontend->>Admin: 显示模板列表
Admin->>Frontend: 选择具体模板
Frontend->>Editor: 加载模板配置
Editor->>Admin: 显示预览效果
Admin->>Frontend: 确认使用模板
Frontend->>Frontend: 填充配置到项目表单
Location: web/apps/lq_label/src/views/user-management-view/
Responsibility: 用户管理界面,显示用户列表和任务统计
Interface:
interface UserManagementViewProps {}
interface UserWithStats {
id: string;
username: string;
email: string;
role: 'admin' | 'annotator' | 'viewer';
created_at: string;
task_stats: {
assigned_count: number;
completed_count: number;
annotation_count: number;
completion_rate: number;
};
}
Features:
Location: web/apps/lq_label/src/components/task-assignment-dialog/
Responsibility: 任务分配对话框
Interface:
interface TaskAssignmentDialogProps {
open: boolean;
taskIds: string[];
onClose: () => void;
onAssign: (userId: string) => Promise<void>;
}
interface AssignableUser {
id: string;
username: string;
email: string;
current_task_count: number;
}
Features:
Location: web/apps/lq_label/src/components/template-gallery/
Responsibility: 模板选择画廊
Interface:
interface TemplateGalleryProps {
onSelect: (template: Template) => void;
selectedId?: string;
}
interface Template {
id: string;
name: string;
category: TemplateCategory;
description: string;
config: string;
preview_image?: string;
tags: string[];
}
type TemplateCategory =
| 'image_classification'
| 'object_detection'
| 'image_segmentation'
| 'text_classification'
| 'ner'
| 'text_labeling'
| 'audio_transcription'
| 'video_annotation';
Features:
Location: web/apps/lq_label/src/components/config-editor/
Responsibility: 标注配置编辑器(代码模式 + 可视化模式)
Interface:
interface ConfigEditorProps {
value: string;
onChange: (value: string) => void;
mode: 'code' | 'visual';
onModeChange: (mode: 'code' | 'visual') => void;
previewData?: Record<string, any>;
}
interface ValidationResult {
valid: boolean;
errors: Array<{
line: number;
column: number;
message: string;
}>;
}
Features:
Location: web/apps/lq_label/src/components/visual-config-builder/
Responsibility: 可视化配置构建器
Interface:
interface VisualConfigBuilderProps {
value: string;
onChange: (value: string) => void;
}
interface ConfigComponent {
type: string;
name: string;
icon: React.ReactNode;
category: 'data' | 'annotation' | 'layout';
defaultProps: Record<string, any>;
}
Features:
Location: web/apps/lq_label/src/components/config-preview/
Responsibility: 配置预览面板
Interface:
interface ConfigPreviewProps {
config: string;
data?: Record<string, any>;
onError?: (error: string) => void;
}
Features:
Location: web/apps/lq_label/src/components/data-export-dialog/
Responsibility: 数据导出对话框
Interface:
interface DataExportDialogProps {
open: boolean;
projectId: string;
onClose: () => void;
}
type ExportFormat = 'json' | 'csv' | 'coco' | 'yolo';
interface ExportOptions {
format: ExportFormat;
status_filter?: 'all' | 'completed' | 'pending' | 'in_progress';
include_metadata: boolean;
}
Features:
Location: web/apps/lq_label/src/components/project-statistics-panel/
Responsibility: 项目统计面板
Interface:
interface ProjectStatisticsPanelProps {
projectId: string;
}
interface ProjectStatistics {
total_tasks: number;
completed_tasks: number;
in_progress_tasks: number;
pending_tasks: number;
total_items: number;
annotated_items: number;
completion_rate: number;
user_stats: UserTaskStats[];
}
interface UserTaskStats {
user_id: string;
username: string;
assigned_tasks: number;
completed_tasks: number;
annotation_count: number;
completion_rate: number;
}
Features:
Router: backend/routers/user.py
New Endpoints:
GET /api/users # List all users (admin only)
GET /api/users/{id} # Get user by ID
GET /api/users/{id}/stats # Get user task statistics
GET /api/users/annotators # List annotators for assignment
Models:
class UserResponse(BaseModel):
id: str
username: str
email: str
role: str
created_at: datetime
class UserWithStatsResponse(UserResponse):
task_stats: TaskStats
class TaskStats(BaseModel):
assigned_count: int
completed_count: int
annotation_count: int
completion_rate: float
Router: backend/routers/task.py (扩展)
New Endpoints:
PUT /api/tasks/{id}/assign # Assign task to user
POST /api/tasks/batch-assign # Batch assign tasks
GET /api/tasks/my-tasks # Get current user's tasks
Models:
class TaskAssignRequest(BaseModel):
user_id: str
class BatchAssignRequest(BaseModel):
task_ids: List[str]
user_ids: List[str]
mode: str = 'round_robin' # 'round_robin' | 'equal'
Router: backend/routers/template.py
Endpoints:
GET /api/templates # List all templates
GET /api/templates/{id} # Get template by ID
GET /api/templates/categories # List template categories
POST /api/templates/validate # Validate XML config
Models:
class TemplateResponse(BaseModel):
id: str
name: str
category: str
description: str
config: str
preview_image: Optional[str]
tags: List[str]
class ConfigValidationRequest(BaseModel):
config: str
class ConfigValidationResponse(BaseModel):
valid: bool
errors: List[ValidationError]
Router: backend/routers/export.py
Endpoints:
POST /api/projects/{id}/export # Export project annotations
GET /api/exports/{id}/status # Get export job status
GET /api/exports/{id}/download # Download export file
Models:
class ExportRequest(BaseModel):
format: str # 'json' | 'csv' | 'coco' | 'yolo'
status_filter: Optional[str]
include_metadata: bool = True
class ExportResponse(BaseModel):
export_id: str
status: str
download_url: Optional[str]
Router: backend/routers/statistics.py
Endpoints:
GET /api/projects/{id}/statistics # Get project statistics
GET /api/statistics/overview # Get overall platform statistics
Models:
class ProjectStatisticsResponse(BaseModel):
total_tasks: int
completed_tasks: int
in_progress_tasks: int
pending_tasks: int
total_items: int
annotated_items: int
completion_rate: float
user_stats: List[UserTaskStats]
-- 预设模板表 (可选,也可以使用静态配置)
CREATE TABLE templates (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
category TEXT NOT NULL,
description TEXT,
config TEXT NOT NULL,
preview_image TEXT,
tags TEXT, -- JSON array
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 导出任务表
CREATE TABLE export_jobs (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
format TEXT NOT NULL,
status TEXT DEFAULT 'pending', -- pending, processing, completed, failed
file_path TEXT,
error_message TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
-- 任务分配历史表 (可选,用于追踪分配记录)
CREATE TABLE task_assignments (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL,
assigned_to TEXT NOT NULL,
assigned_by TEXT NOT NULL,
assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
FOREIGN KEY (assigned_to) REFERENCES users(id),
FOREIGN KEY (assigned_by) REFERENCES users(id)
);
Location: web/apps/lq_label/src/atoms/
// userAtoms.ts
export const usersAtom = atom<UserWithStats[]>([]);
export const annotatorsAtom = atom<AssignableUser[]>([]);
export const userLoadingAtom = atom<boolean>(false);
// templateAtoms.ts
export const templatesAtom = atom<Template[]>([]);
export const selectedTemplateAtom = atom<Template | null>(null);
export const templateCategoriesAtom = atom<TemplateCategory[]>([]);
// configEditorAtoms.ts
export const configValueAtom = atom<string>('');
export const configValidationAtom = atom<ValidationResult | null>(null);
export const editorModeAtom = atom<'code' | 'visual'>('code');
// statisticsAtoms.ts
export const projectStatisticsAtom = atom<ProjectStatistics | null>(null);
export const statisticsLoadingAtom = atom<boolean>(false);
// exportAtoms.ts
export const exportJobAtom = atom<ExportJob | null>(null);
export const exportProgressAtom = atom<number>(0);
模板将以静态配置的形式存储在前端:
// templates/index.ts
export const PREDEFINED_TEMPLATES: Template[] = [
{
id: 'image_classification_basic',
name: '图像分类 - 基础',
category: 'image_classification',
description: '简单的图像分类任务,支持单选分类',
config: `<View>
<Image name="image" value="$image"/>
<Choices name="choice" toName="image">
<Choice value="类别1"/>
<Choice value="类别2"/>
<Choice value="类别3"/>
</Choices>
</View>`,
tags: ['图像', '分类', '单选'],
},
{
id: 'object_detection_bbox',
name: '目标检测 - 边界框',
category: 'object_detection',
description: '使用矩形框标注图像中的目标',
config: `<View>
<Image name="image" value="$image"/>
<RectangleLabels name="label" toName="image">
<Label value="目标1"/>
<Label value="目标2"/>
</RectangleLabels>
</View>`,
tags: ['图像', '检测', '边界框'],
},
// ... 更多模板
];
A property is a characteristic or behavior that should hold true across all valid executions of a system—essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.
For any 角色筛选值或搜索关键词,返回的用户列表中的所有用户都应该满足筛选条件:
Validates: Requirements 1.2, 1.3
For any 任务和用户的分配操作:
Validates: Requirements 2.3, 2.4
For any 任务列表和用户列表的批量分配操作(平均分配模式),分配后每个用户分配的任务数量差异不应超过 1。
Validates: Requirements 2.6
For any 标注人员用户,其任务列表中的所有任务的 assigned_to 字段都应该等于该用户的 ID。任何状态筛选都应该在此基础上进行。
Validates: Requirements 3.1, 3.2
For any 任务,当其所有数据项都被标注后,任务状态应该自动更新为 'completed'。
Validates: Requirements 3.6
For any 选择的模板,确认选择后项目配置字段的值应该等于模板的 config 值。模板搜索结果中的每个模板的名称或描述都应该包含搜索关键词。
Validates: Requirements 4.4, 4.5
For any XML 配置字符串,验证结果应该正确反映其语法有效性:
Validates: Requirements 5.3, 5.4
For any 有效的标注配置:
Validates: Requirements 5.6, 5.8
For any 组件拖拽或属性修改操作,生成的 XML 代码应该正确反映该操作:
Validates: Requirements 6.3, 6.6
For any 无效的标注配置,预览面板应该显示错误信息而不是崩溃或显示空白。
Validates: Requirements 7.5
For any 导出请求:
Validates: Requirements 8.3, 8.4, 8.5
For any 项目或用户:
Validates: Requirements 9.2, 9.3, 9.6, 9.7
API 请求错误
配置验证错误
导出错误
编辑器错误
权限错误
数据验证错误
资源不存在
导出错误
Frontend:
Backend:
配置:
重点属性测试:
// 安装依赖
// npm install @monaco-editor/react
import Editor from '@monaco-editor/react';
const ConfigCodeEditor: React.FC<Props> = ({ value, onChange }) => {
return (
<Editor
height="400px"
language="xml"
value={value}
onChange={(value) => onChange(value || '')}
options={{
minimap: { enabled: false },
lineNumbers: 'on',
wordWrap: 'on',
automaticLayout: true,
}}
/>
);
};
// 使用 DOMParser 进行 XML 验证
function validateXML(xml: string): ValidationResult {
const parser = new DOMParser();
const doc = parser.parseFromString(xml, 'application/xml');
const parseError = doc.querySelector('parsererror');
if (parseError) {
return {
valid: false,
errors: [{
line: 1,
column: 1,
message: parseError.textContent || 'XML 解析错误',
}],
};
}
return { valid: true, errors: [] };
}
# backend/services/export_service.py
def export_to_json(annotations: List[Annotation]) -> str:
return json.dumps([a.to_dict() for a in annotations], indent=2)
def export_to_csv(annotations: List[Annotation]) -> str:
# 使用 pandas 或 csv 模块
pass
def export_to_coco(annotations: List[Annotation]) -> str:
# COCO 格式转换
pass
def export_to_yolo(annotations: List[Annotation]) -> str:
# YOLO 格式转换
pass
# backend/services/statistics_service.py
def calculate_project_statistics(project_id: str) -> ProjectStatistics:
with get_db_connection() as conn:
cursor = conn.cursor()
# 任务统计
cursor.execute("""
SELECT
COUNT(*) as total,
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) as in_progress,
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending
FROM tasks WHERE project_id = ?
""", (project_id,))
# 标注统计
cursor.execute("""
SELECT COUNT(*) as total_annotations
FROM annotations a
JOIN tasks t ON a.task_id = t.id
WHERE t.project_id = ?
""", (project_id,))
# 用户统计
cursor.execute("""
SELECT
u.id, u.username,
COUNT(DISTINCT t.id) as assigned_tasks,
SUM(CASE WHEN t.status = 'completed' THEN 1 ELSE 0 END) as completed_tasks,
COUNT(a.id) as annotation_count
FROM users u
LEFT JOIN tasks t ON t.assigned_to = u.id AND t.project_id = ?
LEFT JOIN annotations a ON a.task_id = t.id AND a.user_id = u.id
WHERE u.role = 'annotator'
GROUP BY u.id, u.username
""", (project_id,))
# 返回统计结果
pass