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:
1 parent
8285400254
commit
5c7c122105
80 files changed
+20074
-1412
No files matched your search
+62
-19
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user