feat: add counting bench, triage, and dataset modules

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.
This commit is contained in:
asus committed 2026-08-14 16:28:52 +07:00
1 parent 8285400254
commit 5c7c122105
80 files changed
+20074 -1412

No files matched your search

+62 -19
View File
@@ -34,6 +34,16 @@ class AutolabelRequest(BaseModel):
min_box_frac: float = 0.0
resume: bool = False
append: bool = False
custom_model_path: Optional[str] = None
class PreviewRequest(BaseModel):
frame_id: int
engine: str
threshold: float = autolabel.DEFAULT_THRESHOLD
iou_threshold: float = autolabel.DEFAULT_IOU
min_box_frac: float = 0.0
target_class_names: Optional[list[str]] = None
custom_model_path: Optional[str] = None
@router.post("/api/projects/{project_id}/batches")
@@ -94,9 +104,10 @@ def start_autolabel(batch_id: int, request: AutolabelRequest) -> dict:
engine_list = request.engines if (request.engines and len(request.engines) > 0) else [request.engine]
return autolabel.start(batch_id, request.threshold, request.iou_threshold,
request.min_box_frac, resume=request.resume, append=request.append,
engines=engine_list, class_ids=request.class_ids,
engine=request.engine, engines=engine_list, class_ids=request.class_ids,
engine_classes=request.engine_classes,
target_class_names=request.target_class_names)
target_class_names=request.target_class_names,
custom_model_path=request.custom_model_path)
except batch_store.BatchError as exc:
raise HTTPException(400, str(exc))
@router.post("/api/batches/inspect-model")
@@ -113,7 +124,7 @@ async def inspect_model(file: UploadFile = File(...)) -> dict:
except Exception as exc:
if os.path.exists(staged_path):
os.unlink(staged_path)
raise HTTPException(400, f"Could not inspect model: {exc}")
raise HTTPException(400, f"Invalid model: {exc}")
@router.post("/api/batches/{batch_id}/autolabel-with-model")
@@ -121,7 +132,7 @@ async def autolabel_with_model(
batch_id: int,
file: UploadFile = File(...),
threshold: float = Form(0.35),
iou_threshold: float = Form(0.8),
iou_threshold: float = Form(0.0),
selected_classes: str = Form("[]"),
append: bool = Form(True),
) -> dict:
@@ -139,6 +150,7 @@ async def autolabel_with_model(
threshold=threshold,
iou_threshold=iou_threshold,
append=append,
engine="custom",
custom_model_path=staged_path,
target_class_names=target_classes,
)
@@ -148,13 +160,39 @@ async def autolabel_with_model(
raise HTTPException(400, f"Auto-annotation failed to start: {exc}")
@router.post("/api/batches/{batch_id}/preview")
def preview_autolabel(batch_id: int, request: PreviewRequest) -> dict:
from backend import autolabel, jobs
if not jobs.gpu_lock.acquire(timeout=20):
busy = jobs.running_types()
kind = busy[0] if busy else "background"
raise HTTPException(409, f"The GPU is busy with a {kind} job — wait for it to finish")
try:
shapes = autolabel.preview_frame(
batch_id=batch_id,
frame_id=request.frame_id,
engine=request.engine,
threshold=request.threshold,
iou_threshold=request.iou_threshold,
min_box_frac=request.min_box_frac,
target_class_names=request.target_class_names,
custom_model_path=request.custom_model_path
)
return {"shapes": shapes}
except Exception as exc:
raise HTTPException(400, str(exc))
finally:
jobs.gpu_lock.release()
@router.post("/api/sam3/playground-test")
async def sam3_playground_test(
file: UploadFile = File(...),
prompts: str = Form(...),
threshold: float = Form(0.35),
iou_threshold: float = Form(0.8),
iou_threshold: float = Form(0.0),
) -> dict:
from PIL import Image
from backend import labeling
from backend.sam3_engine import get_engine
@@ -169,12 +207,20 @@ async def sam3_playground_test(
if not prompt_list:
raise HTTPException(400, "At least one text prompt is required")
from backend import jobs
if not jobs.gpu_lock.acquire(timeout=20):
busy = jobs.running_types()
kind = busy[0] if busy else "background"
raise HTTPException(409, f"The GPU is busy with a {kind} job — wait for it to finish")
try:
engine = get_engine()
raw_dets = engine.detect(image, prompt_list, threshold)
kept_dets = labeling.deduplicate(raw_dets, iou_threshold=iou_threshold)
except Exception as exc:
raise HTTPException(500, f"SAM3 inference failed: {exc}")
finally:
jobs.gpu_lock.release()
results = []
for det in kept_dets:
@@ -213,10 +259,18 @@ def approve_all_batch_frames(batch_id: int) -> dict:
return {"approved_count": updated}
@router.post("/api/batches/{batch_id}/approve")
def approve_batch(batch_id: int) -> dict:
class ApproveRequest(BaseModel):
dataset_id: Optional[int] = None
dataset_name: str = ""
@router.post("/api/batches/{batch_ids}/approve")
def approve_batch(batch_ids: str, request: ApproveRequest = ApproveRequest()) -> dict:
"""`batch_ids` is one id or a comma-separated selection — one merge, one
dataset, however many batches Data Prep was tuned against (REQ-131)."""
try:
return dataset.approve(batch_id)
return dataset.approve(batch_ids, dataset_id=request.dataset_id,
dataset_name=request.dataset_name)
except dataset.DatasetError as exc:
raise HTTPException(400, str(exc))
@@ -227,17 +281,6 @@ def dataset_summary(project_id: int) -> dict:
return dataset.summary(project_id)
@router.get("/api/projects/{project_id}/dataset/download")
def dataset_download(project_id: int):
project = project_or_404(project_id)
try:
path = dataset.zip_path(project)
except dataset.DatasetError as exc:
raise HTTPException(400, str(exc))
return FileResponse(path, media_type="application/zip",
filename=f"{project['slug']}-dataset.zip")
@router.get("/api/frames/{frame_id}/image")
def frame_image(frame_id: int, w: int = 0):
path = batch_store.frame_path(frame_id)