This commit includes major additions and updates to the frontend and backend architectures, introducing new dataset management, live counting features, batch processing, and triage logic. Includes new UI pages, components, and API routes.
115 lines
3.5 KiB
Python
115 lines
3.5 KiB
Python
"""Annotation and review-state routes (REQ-040…045)."""
|
|
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
from pydantic import BaseModel
|
|
|
|
from backend import batches as batch_store
|
|
from backend import review as review_store
|
|
|
|
router = APIRouter(tags=["review"])
|
|
|
|
|
|
class AnnotationRequest(BaseModel):
|
|
class_id: int = 0
|
|
geometry: Dict[str, Any]
|
|
|
|
|
|
class AnnotationPatch(BaseModel):
|
|
class_id: Optional[int] = None
|
|
geometry: Optional[Dict[str, Any]] = None
|
|
|
|
|
|
class BulkRequest(BaseModel):
|
|
annotation_ids: List[int]
|
|
|
|
|
|
class BulkReclassRequest(BaseModel):
|
|
annotation_ids: List[int]
|
|
class_id: int
|
|
|
|
|
|
class StatusRequest(BaseModel):
|
|
status: str
|
|
|
|
|
|
class AssistRequest(BaseModel):
|
|
box: List[float]
|
|
class_id: int = 0
|
|
threshold: float = 0.5
|
|
|
|
|
|
@router.get("/api/frames/{frame_id}/annotations")
|
|
def list_annotations(frame_id: int) -> dict:
|
|
target = review_store.frame(frame_id)
|
|
if target is None:
|
|
raise HTTPException(404, "No such frame")
|
|
return {
|
|
"frame": {
|
|
"id": target["id"], "batch_id": target["batch_id"], "idx": target["idx"],
|
|
"width": target["width"], "height": target["height"],
|
|
"review_status": target["review_status"], "label_type": target["label_type"],
|
|
},
|
|
"annotations": review_store.listing(frame_id),
|
|
}
|
|
|
|
|
|
@router.post("/api/frames/{frame_id}/annotations")
|
|
def add_annotation(frame_id: int, request: AnnotationRequest) -> dict:
|
|
try:
|
|
return review_store.add(frame_id, request.class_id, request.geometry)
|
|
except review_store.ReviewError as exc:
|
|
raise HTTPException(400, str(exc))
|
|
|
|
|
|
@router.patch("/api/annotations/{annotation_id}")
|
|
def patch_annotation(annotation_id: int, request: AnnotationPatch) -> dict:
|
|
try:
|
|
return review_store.update(annotation_id, request.class_id, request.geometry)
|
|
except review_store.ReviewError as exc:
|
|
raise HTTPException(400, str(exc))
|
|
|
|
|
|
@router.delete("/api/annotations/{annotation_id}")
|
|
def delete_annotation(annotation_id: int) -> dict:
|
|
return {"deleted": review_store.delete(annotation_id)}
|
|
|
|
|
|
@router.post("/api/annotations/bulk-delete")
|
|
def bulk_delete(request: BulkRequest) -> dict:
|
|
return {"deleted": review_store.delete_many(request.annotation_ids)}
|
|
|
|
|
|
@router.post("/api/annotations/bulk-reclass")
|
|
def bulk_reclass(request: BulkReclassRequest) -> dict:
|
|
try:
|
|
return {"updated": review_store.reclass_many(request.annotation_ids,
|
|
request.class_id)}
|
|
except review_store.ReviewError as exc:
|
|
raise HTTPException(400, str(exc))
|
|
|
|
|
|
@router.post("/api/frames/{frame_id}/assist")
|
|
def assist(frame_id: int, request: AssistRequest) -> dict:
|
|
try:
|
|
return review_store.assist(frame_id, request.box, request.class_id,
|
|
request.threshold)
|
|
except review_store.ReviewError as exc:
|
|
raise HTTPException(400, str(exc))
|
|
|
|
|
|
@router.post("/api/frames/{frame_id}/status")
|
|
def set_status(frame_id: int, request: StatusRequest) -> dict:
|
|
try:
|
|
return review_store.set_status(frame_id, request.status)
|
|
except review_store.ReviewError as exc:
|
|
raise HTTPException(400, str(exc))
|
|
|
|
|
|
@router.get("/api/batches/{batch_id}/next-pending")
|
|
def next_pending(batch_id: int, after_idx: int = -1) -> dict:
|
|
if batch_store.get(batch_id) is None:
|
|
raise HTTPException(404, "No such batch")
|
|
return {"frame_id": review_store.next_pending(batch_id, after_idx)}
|