Fix classification/face batch actions and snapshot handling

This commit is contained in:
Teagan glenn 2026-02-21 22:41:47 -07:00
parent d175c9758e
commit ae6d6ce2e5
3 changed files with 104 additions and 142 deletions

View File

@ -970,6 +970,17 @@ def categorize_classification_image(request: Request, name: str, body: dict = No
snapshot = get_event_snapshot(event)
if snapshot is None:
return JSONResponse(
content=(
{
"success": False,
"message": f"Failed to read snapshot for event {event_id}.",
}
),
status_code=500,
)
# Get object bounding box for the first detection
if not event.data.get("attributes") or len(event.data["attributes"]) == 0:
return JSONResponse(
@ -987,19 +998,7 @@ def categorize_classification_image(request: Request, name: str, body: dict = No
try:
# Extract the crop from the snapshot
detect_config: DetectConfig = config.cameras[event.camera].detect
frame = cv2.imread(snapshot)
if frame is None:
return JSONResponse(
content=(
{
"success": False,
"message": f"Failed to read snapshot for event {event_id}.",
}
),
status_code=500,
)
frame = snapshot
height, width = frame.shape[:2]

View File

@ -410,73 +410,53 @@ export default function FaceLibrary() {
<FaceSelectionDialog
faceNames={faces}
onTrainAttempt={(name) => {
// Batch train all selected faces
let successCount = 0;
let failCount = 0;
const totalCount = selectedFaces.length;
selectedFaces.forEach((filename, index) => {
const requests = selectedFaces.map((filename) =>
axios
.post(`/faces/train/${name}/classify`, {
training_file: filename,
})
.then((resp) => {
if (resp.status == 200) {
successCount++;
} else {
failCount++;
}
.then(() => true)
.catch(() => false),
);
// Show final toast after all requests complete
if (index === totalCount - 1) {
if (successCount === totalCount) {
toast.success(
t("toast.success.batchTrainedFaces", {
count: successCount,
}),
{
position: "top-center",
},
);
} else if (successCount > 0) {
toast.warning(
t("toast.warning.partialBatchTrained", {
success: successCount,
total: totalCount,
}),
{
position: "top-center",
},
);
} else {
toast.error(
t("toast.error.batchTrainFailed", {
count: totalCount,
}),
{
position: "top-center",
},
);
}
setSelectedFaces([]);
refreshFaces();
}
})
.catch(() => {
failCount++;
if (index === totalCount - 1) {
toast.error(
t("toast.error.batchTrainFailed", {
count: totalCount,
}),
{
position: "top-center",
},
);
setSelectedFaces([]);
refreshFaces();
}
});
Promise.allSettled(requests).then((results) => {
const successCount = results.filter(
(result) => result.status === "fulfilled" && result.value,
).length;
const totalCount = results.length;
if (successCount === totalCount) {
toast.success(
t("toast.success.batchTrainedFaces", {
count: successCount,
}),
{
position: "top-center",
},
);
} else if (successCount > 0) {
toast.warning(
t("toast.warning.partialBatchTrained", {
success: successCount,
total: totalCount,
}),
{
position: "top-center",
},
);
} else {
toast.error(
t("toast.error.batchTrainFailed", {
count: totalCount,
}),
{
position: "top-center",
},
);
}
setSelectedFaces([]);
refreshFaces();
});
}}
>

View File

@ -460,79 +460,62 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
</div>
{pageToggle === "train" && (
<ClassificationSelectionDialog
classes={classes}
classes={Object.keys(dataset || {})}
modelName={model.name}
image={selectedImages[0]}
onRefresh={refreshAll}
onCategorize={(category) => {
// Batch categorize all selected images
let successCount = 0;
let failCount = 0;
const totalCount = selectedImages.length;
selectedImages.forEach((filename, index) => {
const requests = selectedImages.map((filename) =>
axios
.post(`/classification/${model.name}/dataset/categorize`, {
category,
training_file: filename,
})
.then((resp) => {
if (resp.status == 200) {
successCount++;
} else {
failCount++;
}
.post(
`/classification/${model.name}/dataset/categorize`,
{
category,
training_file: filename,
},
)
.then(() => true)
.catch(() => false),
);
// Show final toast after all requests complete
if (index === totalCount - 1) {
if (successCount === totalCount) {
toast.success(
t("toast.success.batchCategorized", {
count: successCount,
}),
{
position: "top-center",
},
);
} else if (successCount > 0) {
toast.warning(
t("toast.warning.partialBatchCategorized", {
success: successCount,
total: totalCount,
}),
{
position: "top-center",
},
);
} else {
toast.error(
t("toast.error.batchCategorizeFailed", {
count: totalCount,
}),
{
position: "top-center",
},
);
}
setSelectedImages([]);
refreshAll();
}
})
.catch(() => {
failCount++;
if (index === totalCount - 1) {
toast.error(
t("toast.error.batchCategorizeFailed", {
count: totalCount,
}),
{
position: "top-center",
},
);
setSelectedImages([]);
refreshAll();
}
});
Promise.allSettled(requests).then((results) => {
const successCount = results.filter(
(result) => result.status === "fulfilled" && result.value,
).length;
const totalCount = results.length;
if (successCount === totalCount) {
toast.success(
t("toast.success.batchCategorized", {
count: successCount,
}),
{
position: "top-center",
},
);
} else if (successCount > 0) {
toast.warning(
t("toast.warning.partialBatchCategorized", {
success: successCount,
total: totalCount,
}),
{
position: "top-center",
},
);
} else {
toast.error(
t("toast.error.batchCategorizeFailed", {
count: totalCount,
}),
{
position: "top-center",
},
);
}
setSelectedImages([]);
refreshAll();
});
}}
>