2026-06-03 11:40:21 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import zipfile
|
|
|
|
|
from typing import Annotated, Any
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile
|
|
|
|
|
from fastapi.responses import FileResponse
|
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
|
|
|
|
from as_platform.auth.deps import require_any_permission, require_permission
|
|
|
|
|
from as_platform.db.models import User
|
|
|
|
|
from as_platform.labeling.annotate import (
|
|
|
|
|
campaign_bootstrap,
|
|
|
|
|
campaign_tasks,
|
|
|
|
|
get_annotation,
|
|
|
|
|
resolve_media_file,
|
|
|
|
|
save_annotation,
|
|
|
|
|
)
|
|
|
|
|
from as_platform.labeling.lock import acquire_lock, release_lock, renew_lock
|
|
|
|
|
from as_platform.labeling.progress import (
|
|
|
|
|
assign_tasks_even,
|
|
|
|
|
assign_tasks_explicit,
|
2026-06-15 17:25:28 +08:00
|
|
|
assign_tasks_quantized,
|
|
|
|
|
campaign_my_tasks,
|
2026-06-03 11:40:21 +08:00
|
|
|
campaign_progress,
|
2026-06-15 17:25:28 +08:00
|
|
|
list_my_assignments,
|
2026-06-03 11:40:21 +08:00
|
|
|
release_task_assignment,
|
|
|
|
|
reassign_task,
|
|
|
|
|
user_is_coordinator,
|
|
|
|
|
)
|
|
|
|
|
from as_platform.labeling.service import (
|
|
|
|
|
assign_campaign,
|
|
|
|
|
get_campaign,
|
|
|
|
|
list_campaign_export_jobs,
|
|
|
|
|
list_labeling_assignees,
|
|
|
|
|
list_labeling_batches,
|
|
|
|
|
open_campaign,
|
|
|
|
|
submit_campaign,
|
|
|
|
|
trigger_labeling_export,
|
|
|
|
|
)
|
|
|
|
|
from as_platform.labeling.vendor_import import import_vendor_zip, list_registry_profiles
|
|
|
|
|
|
|
|
|
|
router = APIRouter(tags=["labeling"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OpenCampaignBody(BaseModel):
|
2026-06-15 17:25:28 +08:00
|
|
|
project: str = Field(..., pattern="^(dms|lane|adas)$")
|
2026-06-03 11:40:21 +08:00
|
|
|
task: str
|
|
|
|
|
batch: str
|
|
|
|
|
mode: str | None = None
|
|
|
|
|
pack: str | None = None
|
|
|
|
|
location: str = "inbox"
|
2026-06-15 17:25:28 +08:00
|
|
|
annotation_types: list[str] | None = None
|
2026-06-03 11:40:21 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class AssignCampaignBody(BaseModel):
|
|
|
|
|
user_id: int | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AnnotationBody(BaseModel):
|
|
|
|
|
result: list[dict[str, Any]] | dict[str, Any] | None = None
|
|
|
|
|
annotations: list[dict[str, Any]] | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AssignTasksExplicitItem(BaseModel):
|
|
|
|
|
user_id: int
|
|
|
|
|
task_ids: list[str] = Field(default_factory=list)
|
|
|
|
|
|
|
|
|
|
|
2026-06-15 17:25:28 +08:00
|
|
|
class AssignTasksQuantizedItem(BaseModel):
|
|
|
|
|
user_id: int
|
|
|
|
|
count: int = 0
|
|
|
|
|
|
|
|
|
|
|
2026-06-03 11:40:21 +08:00
|
|
|
class AssignTasksBody(BaseModel):
|
|
|
|
|
mode: str = "even"
|
|
|
|
|
user_ids: list[int] | None = None
|
|
|
|
|
items: list[AssignTasksExplicitItem] | None = None
|
2026-06-15 17:25:28 +08:00
|
|
|
quantized_items: list[AssignTasksQuantizedItem] | None = None
|
2026-06-03 11:40:21 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class ReassignTaskBody(BaseModel):
|
|
|
|
|
user_id: int
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/v1/labeling/assignees")
|
|
|
|
|
def api_labeling_assignees(
|
|
|
|
|
_user: Annotated[User, Depends(require_permission("read:pending"))],
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return list_labeling_assignees()
|
|
|
|
|
|
|
|
|
|
|
2026-06-15 17:25:28 +08:00
|
|
|
@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
|
|
|
|
|
|
|
|
|
|
|
2026-06-03 11:40:21 +08:00
|
|
|
@router.patch("/api/v1/labeling/campaigns/{campaign_id}/assign")
|
|
|
|
|
def api_assign_campaign(
|
|
|
|
|
campaign_id: str,
|
|
|
|
|
body: AssignCampaignBody,
|
|
|
|
|
_user: Annotated[User, Depends(require_permission("read:pending"))],
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
try:
|
|
|
|
|
return assign_campaign(campaign_id, body.user_id)
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
raise HTTPException(404, "campaign not found") from None
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
raise HTTPException(400, str(e)) from e
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/v1/labeling/batches")
|
|
|
|
|
def api_labeling_batches(
|
|
|
|
|
_user: Annotated[User, Depends(require_permission("read:pending"))],
|
|
|
|
|
stage: str | None = Query(None),
|
|
|
|
|
offset: int = Query(0, ge=0),
|
|
|
|
|
limit: int = Query(20, ge=1, le=100),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return list_labeling_batches(stage=stage, offset=offset, limit=limit)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/api/v1/labeling/campaigns/open")
|
|
|
|
|
def api_open_campaign(
|
|
|
|
|
body: OpenCampaignBody,
|
|
|
|
|
_user: Annotated[User, Depends(require_permission("read:pending"))],
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return open_campaign(
|
|
|
|
|
project=body.project,
|
|
|
|
|
task=body.task,
|
|
|
|
|
batch=body.batch,
|
|
|
|
|
mode=body.mode,
|
|
|
|
|
pack=body.pack,
|
|
|
|
|
location=body.location,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/v1/labeling/campaigns/{campaign_id}")
|
|
|
|
|
def api_get_campaign(
|
|
|
|
|
campaign_id: str,
|
|
|
|
|
_user: Annotated[User, Depends(require_permission("read:pending"))],
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
row = get_campaign(campaign_id)
|
|
|
|
|
if not row:
|
|
|
|
|
raise HTTPException(404, "campaign not found")
|
|
|
|
|
return row
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/v1/labeling/campaigns/{campaign_id}/bootstrap")
|
|
|
|
|
def api_campaign_bootstrap(
|
|
|
|
|
campaign_id: str,
|
|
|
|
|
_user: Annotated[User, Depends(require_permission("read:pending"))],
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
try:
|
|
|
|
|
return campaign_bootstrap(campaign_id)
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
raise HTTPException(404, "campaign not found") from None
|
|
|
|
|
except Exception as e:
|
|
|
|
|
raise HTTPException(400, str(e)) from e
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/v1/labeling/campaigns/{campaign_id}/progress")
|
|
|
|
|
def api_campaign_progress(
|
|
|
|
|
campaign_id: str,
|
|
|
|
|
_user: Annotated[User, Depends(require_permission("read:pending"))],
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
try:
|
|
|
|
|
return campaign_progress(campaign_id)
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
raise HTTPException(404, "campaign not found") from None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/api/v1/labeling/campaigns/{campaign_id}/assign-tasks")
|
|
|
|
|
def api_assign_tasks(
|
|
|
|
|
campaign_id: str,
|
|
|
|
|
body: AssignTasksBody,
|
|
|
|
|
user: Annotated[User, Depends(require_any_permission("write:labeling_assign", "read:pending"))],
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
if not user_is_coordinator(user):
|
|
|
|
|
raise HTTPException(403, "仅协调员可分配任务")
|
|
|
|
|
try:
|
|
|
|
|
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)
|
2026-06-15 17:25:28 +08:00
|
|
|
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)
|
2026-06-03 11:40:21 +08:00
|
|
|
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)
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
raise HTTPException(404, "campaign not found") from None
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
raise HTTPException(400, str(e)) from e
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.patch("/api/v1/labeling/campaigns/{campaign_id}/assignments/{task_id}")
|
|
|
|
|
def api_reassign_task(
|
|
|
|
|
campaign_id: str,
|
|
|
|
|
task_id: str,
|
|
|
|
|
body: ReassignTaskBody,
|
|
|
|
|
user: Annotated[User, Depends(require_any_permission("write:labeling_assign", "read:pending"))],
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
if not user_is_coordinator(user):
|
|
|
|
|
raise HTTPException(403, "仅协调员可改派任务")
|
|
|
|
|
try:
|
|
|
|
|
return reassign_task(campaign_id, task_id, body.user_id)
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
raise HTTPException(404, "assignment not found") from None
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
raise HTTPException(400, str(e)) from e
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/api/v1/labeling/campaigns/{campaign_id}/assignments/{task_id}")
|
|
|
|
|
def api_release_assignment(
|
|
|
|
|
campaign_id: str,
|
|
|
|
|
task_id: str,
|
|
|
|
|
user: Annotated[User, Depends(require_any_permission("write:labeling_assign", "read:pending"))],
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
if not user_is_coordinator(user):
|
|
|
|
|
raise HTTPException(403, "仅协调员可释放任务")
|
|
|
|
|
try:
|
|
|
|
|
return release_task_assignment(campaign_id, task_id)
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
raise HTTPException(404, "assignment not found") from None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/v1/labeling/campaigns/{campaign_id}/tasks")
|
|
|
|
|
def api_campaign_tasks(
|
|
|
|
|
campaign_id: str,
|
|
|
|
|
user: Annotated[User, Depends(require_permission("read:pending"))],
|
|
|
|
|
offset: int = Query(0, ge=0),
|
|
|
|
|
limit: int = Query(50, ge=1, le=200),
|
|
|
|
|
assignee: str | None = Query(None),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
try:
|
|
|
|
|
eff = assignee
|
|
|
|
|
if eff is None and not user_is_coordinator(user):
|
|
|
|
|
eff = "me"
|
|
|
|
|
return campaign_tasks(campaign_id, offset=offset, limit=limit, user=user, assignee=eff)
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
raise HTTPException(404, "campaign not found") from None
|
|
|
|
|
except PermissionError as e:
|
|
|
|
|
raise HTTPException(403, str(e)) from e
|
|
|
|
|
except Exception as e:
|
|
|
|
|
raise HTTPException(400, str(e)) from e
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/v1/labeling/media/{campaign_id}/{file_path:path}")
|
|
|
|
|
def api_labeling_media(
|
|
|
|
|
campaign_id: str,
|
|
|
|
|
file_path: str,
|
|
|
|
|
_user: Annotated[User, Depends(require_permission("read:pending"))],
|
|
|
|
|
):
|
|
|
|
|
try:
|
|
|
|
|
target = resolve_media_file(campaign_id, file_path)
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
raise HTTPException(404, "not found") from None
|
|
|
|
|
except PermissionError:
|
|
|
|
|
raise HTTPException(403, "forbidden") from None
|
|
|
|
|
except Exception as e:
|
|
|
|
|
raise HTTPException(400, str(e)) from e
|
|
|
|
|
return FileResponse(target)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/v1/labeling/campaigns/{campaign_id}/annotations/{task_id}")
|
|
|
|
|
def api_get_annotation(
|
|
|
|
|
campaign_id: str,
|
|
|
|
|
task_id: str,
|
|
|
|
|
_user: Annotated[User, Depends(require_permission("read:pending"))],
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
try:
|
|
|
|
|
return get_annotation(campaign_id, task_id)
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
raise HTTPException(404, "campaign not found") from None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/api/v1/labeling/campaigns/{campaign_id}/annotations/{task_id}")
|
|
|
|
|
def api_put_annotation(
|
|
|
|
|
campaign_id: str,
|
|
|
|
|
task_id: str,
|
|
|
|
|
body: AnnotationBody,
|
|
|
|
|
user: Annotated[User, Depends(require_permission("read:pending"))],
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
try:
|
|
|
|
|
return save_annotation(campaign_id, task_id, body.model_dump(exclude_none=True), user=user)
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
raise HTTPException(404, "campaign not found") from None
|
|
|
|
|
except PermissionError as e:
|
|
|
|
|
raise HTTPException(403, str(e)) from e
|
|
|
|
|
except Exception as e:
|
|
|
|
|
raise HTTPException(400, str(e)) from e
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/api/v1/labeling/campaigns/{campaign_id}/export")
|
|
|
|
|
def api_labeling_export(
|
|
|
|
|
campaign_id: str,
|
|
|
|
|
_user: Annotated[User, Depends(require_permission("read:pending"))],
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
try:
|
|
|
|
|
return trigger_labeling_export(campaign_id)
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
raise HTTPException(404, "campaign not found") from None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/v1/labeling/campaigns/{campaign_id}/export-jobs")
|
|
|
|
|
def api_campaign_export_jobs(
|
|
|
|
|
campaign_id: str,
|
|
|
|
|
_user: Annotated[User, Depends(require_permission("read:pending"))],
|
|
|
|
|
limit: int = Query(30, ge=1, le=100),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
if not get_campaign(campaign_id):
|
|
|
|
|
raise HTTPException(404, "campaign not found")
|
|
|
|
|
return list_campaign_export_jobs(campaign_id, limit=limit)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/api/v1/labeling/campaigns/{campaign_id}/submit")
|
|
|
|
|
def api_campaign_submit(
|
|
|
|
|
campaign_id: str,
|
|
|
|
|
_user: Annotated[User, Depends(require_permission("read:pending"))],
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
try:
|
|
|
|
|
return submit_campaign(campaign_id)
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
raise HTTPException(400, str(e)) from None
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
raise HTTPException(404, "campaign not found") from None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/v1/labeling/registry-profiles")
|
|
|
|
|
def api_labeling_registry_profiles(
|
|
|
|
|
_user: Annotated[User, Depends(require_permission("read:pending"))],
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return list_registry_profiles()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/api/v1/labeling/campaigns/{campaign_id}/import-vendor")
|
|
|
|
|
async def api_import_vendor(
|
|
|
|
|
campaign_id: str,
|
|
|
|
|
_user: Annotated[User, Depends(require_permission("write:labeling_vendor"))],
|
|
|
|
|
file: UploadFile = File(...),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
raw = await file.read()
|
|
|
|
|
try:
|
|
|
|
|
return import_vendor_zip(campaign_id, raw)
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
raise HTTPException(404, "campaign not found") from None
|
|
|
|
|
except (zipfile.BadZipFile, ValueError) as e:
|
|
|
|
|
raise HTTPException(400, str(e)) from e
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/api/v1/labeling/campaigns/{campaign_id}/tasks/{task_id}/lock")
|
|
|
|
|
def api_labeling_lock_acquire(
|
|
|
|
|
campaign_id: str,
|
|
|
|
|
task_id: str,
|
|
|
|
|
user: Annotated[User, Depends(require_permission("read:pending"))],
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
if not get_campaign(campaign_id):
|
|
|
|
|
raise HTTPException(404, "campaign not found")
|
|
|
|
|
result = acquire_lock(campaign_id, task_id, user_id=user.id, user_name=user.name)
|
|
|
|
|
if not result.get("ok"):
|
|
|
|
|
raise HTTPException(409, detail=result)
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/api/v1/labeling/campaigns/{campaign_id}/tasks/{task_id}/lock")
|
|
|
|
|
def api_labeling_lock_release(
|
|
|
|
|
campaign_id: str,
|
|
|
|
|
task_id: str,
|
|
|
|
|
user: Annotated[User, Depends(require_permission("read:pending"))],
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
result = release_lock(campaign_id, task_id, user_id=user.id)
|
|
|
|
|
if not result.get("ok"):
|
|
|
|
|
raise HTTPException(409, detail=result)
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/api/v1/labeling/campaigns/{campaign_id}/tasks/{task_id}/lock/renew")
|
|
|
|
|
def api_labeling_lock_renew(
|
|
|
|
|
campaign_id: str,
|
|
|
|
|
task_id: str,
|
|
|
|
|
user: Annotated[User, Depends(require_permission("read:pending"))],
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
result = renew_lock(campaign_id, task_id, user_id=user.id)
|
|
|
|
|
if not result.get("ok"):
|
|
|
|
|
raise HTTPException(409, detail=result)
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
2026-06-15 17:25:28 +08:00
|
|
|
# ── 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
|
|
|
|
|
|
|
|
|
|
|
2026-06-03 11:40:21 +08:00
|
|
|
# ── 标注质检 (Quality Review) ──
|
|
|
|
|
|
|
|
|
|
class ReviewScoreBody(BaseModel):
|
|
|
|
|
image_path: str
|
|
|
|
|
score: str # good / fine / bad
|
|
|
|
|
comment: str | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ReviewBatchBody(BaseModel):
|
|
|
|
|
scores: list[ReviewScoreBody]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/v1/labeling/campaigns/{campaign_id}/review-queue")
|
|
|
|
|
def api_review_queue(
|
|
|
|
|
campaign_id: str,
|
|
|
|
|
_user: Annotated[User, Depends(require_permission("write:approval_review"))],
|
|
|
|
|
offset: int = Query(0, ge=0),
|
|
|
|
|
limit: int = Query(20, ge=1, le=100),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
from as_platform.audit.review import get_review_queue
|
|
|
|
|
return get_review_queue(campaign_id, offset=offset, limit=limit)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/v1/labeling/campaigns/{campaign_id}/review-image")
|
|
|
|
|
def api_review_image(
|
|
|
|
|
campaign_id: str,
|
|
|
|
|
_user: Annotated[User, Depends(require_permission("write:approval_review"))],
|
|
|
|
|
path: str = Query(...),
|
|
|
|
|
) -> FileResponse:
|
|
|
|
|
from as_platform.audit.review import get_review_image
|
|
|
|
|
import tempfile
|
|
|
|
|
try:
|
|
|
|
|
# Get class names from registry
|
|
|
|
|
import yaml
|
|
|
|
|
from as_platform.data.core import load_wf, proj_root
|
|
|
|
|
wf = load_wf()
|
|
|
|
|
root = proj_root(wf, "dms")
|
|
|
|
|
reg = yaml.safe_load((root / wf["projects"]["dms"]["registry"]).read_text())
|
|
|
|
|
# Build class_names dict from campaign scope
|
|
|
|
|
class_names: dict[int, str] = {}
|
|
|
|
|
# Try to get from the specific task
|
|
|
|
|
tasks = reg.get("tasks", {})
|
|
|
|
|
for task_cfg in tasks.values():
|
|
|
|
|
names = task_cfg.get("names")
|
|
|
|
|
if isinstance(names, list):
|
|
|
|
|
for i, n in enumerate(names):
|
|
|
|
|
class_names[i] = n
|
|
|
|
|
|
|
|
|
|
data = get_review_image(campaign_id, path, class_names)
|
|
|
|
|
tmp = tempfile.NamedTemporaryFile(suffix=".jpg", delete=False)
|
|
|
|
|
tmp.write(data)
|
|
|
|
|
tmp.close()
|
|
|
|
|
return FileResponse(tmp.name, media_type="image/jpeg")
|
|
|
|
|
except FileNotFoundError as e:
|
|
|
|
|
raise HTTPException(404, str(e)) from e
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/api/v1/labeling/campaigns/{campaign_id}/review-submit")
|
|
|
|
|
def api_review_submit(
|
|
|
|
|
campaign_id: str,
|
|
|
|
|
body: ReviewBatchBody,
|
|
|
|
|
user: Annotated[User, Depends(require_permission("write:approval_review"))],
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
from as_platform.audit.review import submit_review_scores
|
|
|
|
|
items = [s.model_dump() for s in body.scores]
|
|
|
|
|
return submit_review_scores(campaign_id, items, reviewer_user_id=user.id, reviewer_name=user.name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/v1/labeling/campaigns/{campaign_id}/review-progress")
|
|
|
|
|
def api_review_progress(
|
|
|
|
|
campaign_id: str,
|
|
|
|
|
_user: Annotated[User, Depends(require_permission("read:pending"))],
|
|
|
|
|
) -> dict[str, int]:
|
|
|
|
|
from as_platform.audit.review import review_progress
|
|
|
|
|
return review_progress(campaign_id)
|