Miscellaneous Fixes (#20848)

* Fix filtering for classification

* Adjust prompt to account for response tokens

* Correctly return response for reprocess

* Use API response to update data instead of trying to re-parse all of the values

* Implement rename class api

* Fix model deletion / rename dialog

* Remove camera spatial context

* Catch error
This commit is contained in:
Nicolas Mowen
2025-11-08 13:13:40 -07:00
committed by GitHub
parent c99ada8f6a
commit d41ee4ff88
11 changed files with 255 additions and 117 deletions

View File

@@ -67,9 +67,6 @@
},
"activity_context_prompt": {
"label": "Custom activity context prompt defining normal activity patterns for this property."
},
"camera_context": {
"label": "Spatial context about the camera's field of view to help with descriptive accuracy. Should describe physical features and locations outside the frame. This is for spatial reference only and should NOT include subjective assessments."
}
}
}

View File

@@ -22,7 +22,8 @@
"categorizedImage": "Successfully Classified Image",
"trainedModel": "Successfully trained model.",
"trainingModel": "Successfully started model training.",
"updatedModel": "Successfully updated model configuration"
"updatedModel": "Successfully updated model configuration",
"renamedCategory": "Successfully renamed class to {{name}}"
},
"error": {
"deleteImageFailed": "Failed to delete: {{errorMessage}}",
@@ -30,7 +31,8 @@
"deleteModelFailed": "Failed to delete model: {{errorMessage}}",
"categorizeFailed": "Failed to categorize image: {{errorMessage}}",
"trainingFailed": "Failed to start model training: {{errorMessage}}",
"updateModelFailed": "Failed to update model: {{errorMessage}}"
"updateModelFailed": "Failed to update model: {{errorMessage}}",
"renameCategoryFailed": "Failed to rename class: {{errorMessage}}"
}
},
"deleteCategory": {

View File

@@ -75,7 +75,7 @@
"deletedName_other": "{{count}} faces have been successfully deleted.",
"renamedFace": "Successfully renamed face to {{name}}",
"trainedFace": "Successfully trained face.",
"updatedFaceScore": "Successfully updated face score."
"updatedFaceScore": "Successfully updated face score to {{name}} ({{score}})."
},
"error": {
"uploadingImageFailed": "Failed to upload image: {{errorMessage}}",

View File

@@ -622,7 +622,15 @@ type TrainingGridProps = {
faceNames: string[];
selectedFaces: string[];
onClickFaces: (images: string[], ctrl: boolean) => void;
onRefresh: () => void;
onRefresh: (
data?:
| FaceLibraryData
| Promise<FaceLibraryData>
| ((
currentData: FaceLibraryData | undefined,
) => FaceLibraryData | undefined),
opts?: boolean | { revalidate?: boolean },
) => Promise<FaceLibraryData | undefined>;
};
function TrainingGrid({
config,
@@ -726,7 +734,15 @@ type FaceAttemptGroupProps = {
faceNames: string[];
selectedFaces: string[];
onClickFaces: (image: string[], ctrl: boolean) => void;
onRefresh: () => void;
onRefresh: (
data?:
| FaceLibraryData
| Promise<FaceLibraryData>
| ((
currentData: FaceLibraryData | undefined,
) => FaceLibraryData | undefined),
opts?: boolean | { revalidate?: boolean },
) => Promise<FaceLibraryData | undefined>;
};
function FaceAttemptGroup({
config,
@@ -814,11 +830,44 @@ function FaceAttemptGroup({
axios
.post(`/faces/reprocess`, { training_file: data.filename })
.then((resp) => {
if (resp.status == 200) {
toast.success(t("toast.success.updatedFaceScore"), {
position: "top-center",
});
onRefresh();
if (resp.status == 200 && resp.data?.success) {
const { face_name, score } = resp.data;
const oldFilename = data.filename;
const parts = oldFilename.split("-");
const newFilename = `${parts[0]}-${parts[1]}-${parts[2]}-${face_name}-${score}.webp`;
onRefresh(
(currentData: FaceLibraryData | undefined) => {
if (!currentData?.train) return currentData;
return {
...currentData,
train: currentData.train.map((filename: string) =>
filename === oldFilename ? newFilename : filename,
),
};
},
{ revalidate: true },
);
toast.success(
t("toast.success.updatedFaceScore", {
name: face_name,
score: score.toFixed(2),
}),
{
position: "top-center",
},
);
} else if (resp.data?.success === false) {
// Handle case where API returns success: false
const errorMessage = resp.data?.message || "Unknown error";
toast.error(
t("toast.error.updateFaceScoreFailed", { errorMessage }),
{
position: "top-center",
},
);
}
})
.catch((error) => {

View File

@@ -187,6 +187,37 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
null,
);
const onRename = useCallback(
(old_name: string, new_name: string) => {
axios
.put(`/classification/${model.name}/dataset/${old_name}/rename`, {
new_category: new_name,
})
.then((resp) => {
if (resp.status == 200) {
toast.success(
t("toast.success.renamedCategory", { name: new_name }),
{
position: "top-center",
},
);
setPageToggle(new_name);
refreshDataset();
}
})
.catch((error) => {
const errorMessage =
error.response?.data?.message ||
error.response?.data?.detail ||
"Unknown error";
toast.error(t("toast.error.renameCategoryFailed", { errorMessage }), {
position: "top-center",
});
});
},
[model, setPageToggle, refreshDataset, t],
);
const onDelete = useCallback(
(ids: string[], isName: boolean = false, category?: string) => {
const targetCategory = category || pageToggle;
@@ -354,7 +385,7 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
trainImages={trainImages || []}
setPageToggle={setPageToggle}
onDelete={onDelete}
onRename={() => {}}
onRename={onRename}
/>
</div>
)}
@@ -534,7 +565,7 @@ function LibrarySelector({
regexErrorMessage={t("description.invalidName")}
/>
<DropdownMenu>
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button className="flex justify-between smart-capitalize">
{pageTitle}
@@ -585,48 +616,50 @@ function LibrarySelector({
({dataset?.[id].length})
</span>
</div>
<div className="flex gap-0.5">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-7 lg:opacity-0 lg:transition-opacity lg:group-hover:opacity-100"
onClick={(e) => {
e.stopPropagation();
setRenameClass(id);
}}
>
<LuPencil className="size-4 text-primary" />
</Button>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent>
{t("button.renameCategory")}
</TooltipContent>
</TooltipPortal>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-7 lg:opacity-0 lg:transition-opacity lg:group-hover:opacity-100"
onClick={(e) => {
e.stopPropagation();
setConfirmDelete(id);
}}
>
<LuTrash2 className="size-4 text-destructive" />
</Button>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent>
{t("button.deleteCategory")}
</TooltipContent>
</TooltipPortal>
</Tooltip>
</div>
{id != "none" && (
<div className="flex gap-0.5">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-7 lg:opacity-0 lg:transition-opacity lg:group-hover:opacity-100"
onClick={(e) => {
e.stopPropagation();
setRenameClass(id);
}}
>
<LuPencil className="size-4 text-primary" />
</Button>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent>
{t("button.renameCategory")}
</TooltipContent>
</TooltipPortal>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-7 lg:opacity-0 lg:transition-opacity lg:group-hover:opacity-100"
onClick={(e) => {
e.stopPropagation();
setConfirmDelete(id);
}}
>
<LuTrash2 className="size-4 text-destructive" />
</Button>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent>
{t("button.deleteCategory")}
</TooltipContent>
</TooltipPortal>
</Tooltip>
</div>
)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
@@ -745,17 +778,11 @@ function TrainGrid({
return false;
}
if (
trainFilter.min_score &&
trainFilter.min_score > data.score / 100.0
) {
if (trainFilter.min_score && trainFilter.min_score > data.score) {
return false;
}
if (
trainFilter.max_score &&
trainFilter.max_score < data.score / 100.0
) {
if (trainFilter.max_score && trainFilter.max_score < data.score) {
return false;
}