feat: CVAT 标注引擎、我的标注收件箱与 ADAS Cuboid 送标

- 统一标注引擎为 CVAT:客户端/配置/格式转换、iframe 标注页、docker-compose.cvat.yml 与 no_auth 补丁
- 移除 Label Studio 相关配置与构建脚本,清理 embedded.bak 备份与误提交的 node_modules
- 新增「我的标注」:跨 Campaign 收件箱、逐张清单、CVAT frame 跳转
- 飞书任务分配:通讯录同步选人、按量分配、分配后 DM 通知(含 my-tasks 链接)
- ADAS cuboid_7cls 数据湖接入:workflow 路径、register-batch、开标上传与标注同步
- 数据湖挂载 AS_DATA_LAKE_ROOT、datasets/adas 符号链接、reset_labeling 运维脚本
- 补充 docs/HANDOVER.md 项目交接文档

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-15 17:25:28 +08:00
parent e72bc061c5
commit 672ef61e17
5281 changed files with 5194 additions and 1315918 deletions

View File

@@ -20,7 +20,10 @@ from as_platform.labeling.lock import acquire_lock, release_lock, renew_lock
from as_platform.labeling.progress import (
assign_tasks_even,
assign_tasks_explicit,
assign_tasks_quantized,
campaign_my_tasks,
campaign_progress,
list_my_assignments,
release_task_assignment,
reassign_task,
user_is_coordinator,
@@ -41,12 +44,13 @@ router = APIRouter(tags=["labeling"])
class OpenCampaignBody(BaseModel):
project: str = Field(..., pattern="^(dms|lane)$")
project: str = Field(..., pattern="^(dms|lane|adas)$")
task: str
batch: str
mode: str | None = None
pack: str | None = None
location: str = "inbox"
annotation_types: list[str] | None = None
class AssignCampaignBody(BaseModel):
@@ -63,10 +67,16 @@ class AssignTasksExplicitItem(BaseModel):
task_ids: list[str] = Field(default_factory=list)
class AssignTasksQuantizedItem(BaseModel):
user_id: int
count: int = 0
class AssignTasksBody(BaseModel):
mode: str = "even"
user_ids: list[int] | None = None
items: list[AssignTasksExplicitItem] | None = None
quantized_items: list[AssignTasksQuantizedItem] | None = None
class ReassignTaskBody(BaseModel):
@@ -80,6 +90,24 @@ def api_labeling_assignees(
return list_labeling_assignees()
@router.get("/api/v1/labeling/my-assignments")
def api_my_assignments(
user: Annotated[User, Depends(require_permission("read:pending"))],
) -> dict[str, Any]:
return list_my_assignments(user.id)
@router.get("/api/v1/labeling/campaigns/{campaign_id}/my-tasks")
def api_campaign_my_tasks(
campaign_id: str,
user: Annotated[User, Depends(require_permission("read:pending"))],
) -> dict[str, Any]:
try:
return campaign_my_tasks(campaign_id, user.id)
except FileNotFoundError:
raise HTTPException(404, "campaign not found") from None
@router.patch("/api/v1/labeling/campaigns/{campaign_id}/assign")
def api_assign_campaign(
campaign_id: str,
@@ -166,6 +194,9 @@ def api_assign_tasks(
if body.mode == "explicit":
items = [{"user_id": i.user_id, "task_ids": i.task_ids} for i in (body.items or [])]
return assign_tasks_explicit(campaign_id, items, assigned_by_user_id=user.id)
if body.mode == "quantized":
items = [{"user_id": i.user_id, "count": i.count} for i in (body.quantized_items or [])]
return assign_tasks_quantized(campaign_id, items, assigned_by_user_id=user.id)
if not body.user_ids:
raise ValueError("even 模式需要 user_ids")
return assign_tasks_even(campaign_id, body.user_ids, assigned_by_user_id=user.id)
@@ -368,6 +399,37 @@ def api_labeling_lock_renew(
return result
# ── CVAT 集成端点 ──
@router.get("/api/v1/labeling/cvat/status/{campaign_id}")
def api_cvat_status(
campaign_id: str,
_user: Annotated[User, Depends(require_permission("read:pending"))],
) -> dict[str, Any]:
from as_platform.labeling.service import get_cvat_status
try:
return get_cvat_status(campaign_id)
except FileNotFoundError:
raise HTTPException(404, "campaign not found") from None
@router.post("/api/v1/labeling/cvat/sync/{campaign_id}")
def api_cvat_sync(
campaign_id: str,
_user: Annotated[User, Depends(require_permission("read:pending"))],
) -> dict[str, Any]:
from as_platform.labeling.service import sync_cvat_annotations
try:
return sync_cvat_annotations(campaign_id)
except FileNotFoundError:
raise HTTPException(404, "campaign not found") from None
except ValueError as e:
raise HTTPException(400, str(e)) from e
except Exception as e:
raise HTTPException(500, f"CVAT 同步失败: {e}") from e
# ── 标注质检 (Quality Review) ──
class ReviewScoreBody(BaseModel):

View File

@@ -708,18 +708,14 @@ def api_scan_inbox(
if batch_name in registered:
continue # 已登记
# Count images
img_count = 0
lbl_count = 0
has_labels = False
for ext in ["*.jpg", "*.jpeg", "*.png", "*.bmp"]:
for _ in batch_dir.glob(ext):
img_count += 1
if (batch_dir / "labels").is_dir():
has_labels = True
for ext in ["*.txt", "*.json"]:
for _ in (batch_dir / "labels").glob(ext):
lbl_count += 1
# Count images (含 images/ 子目录)
from as_platform.data.batch import count_images, count_label_files, dms_has_labels
img_count = count_images(batch_dir)
if not img_count and (batch_dir / "images").is_dir():
img_count = count_images(batch_dir / "images")
lbl_count = count_label_files(batch_dir / "labels") if (batch_dir / "labels").is_dir() else 0
has_labels = lbl_count > 0 or dms_has_labels(batch_dir)
items.append({
"project": project,
@@ -879,9 +875,6 @@ def _mount_ui() -> None:
assets = ui / "assets"
if assets.is_dir():
app.mount("/assets", StaticFiles(directory=str(assets)), name="ui-assets")
annotate = ui / "annotate"
if annotate.is_dir():
app.mount("/annotate", StaticFiles(directory=str(annotate), html=True), name="ui-annotate")
return
@@ -889,14 +882,10 @@ _mount_ui()
@app.get("/labeling/campaigns/{campaign_id}/annotate", include_in_schema=False)
def serve_annotate_app(campaign_id: str):
"""标注编辑器页面返回旧 Label Studio 构建产物"""
if not _UI_DIR:
raise HTTPException(404, "UI not built")
annotate_index = _UI_DIR / "annotate" / "index.html"
if annotate_index.is_file():
return FileResponse(annotate_index)
raise HTTPException(404, "标注编辑器未构建")
def serve_annotate_app_redirect(campaign_id: str):
"""标注编辑器 — 已迁移至 CVAT302 重定向到新路由"""
from fastapi.responses import RedirectResponse
return RedirectResponse(f"/labeling/annotate/{campaign_id}", status_code=302)
@app.get("/{full_path:path}", include_in_schema=False)