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:
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
"""标注编辑器 — 已迁移至 CVAT,302 重定向到新路由"""
|
||||
from fastapi.responses import RedirectResponse
|
||||
return RedirectResponse(f"/labeling/annotate/{campaign_id}", status_code=302)
|
||||
|
||||
|
||||
@app.get("/{full_path:path}", include_in_schema=False)
|
||||
|
||||
Reference in New Issue
Block a user