mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-01 19:17:41 +03:00
Add support for deleting images
This commit is contained in:
parent
3420de88be
commit
234c65b5c3
@ -497,3 +497,75 @@ async def train_configured_model(
|
|||||||
content={"success": True, "message": "Started classification model training."},
|
content={"success": True, "message": "Started classification model training."},
|
||||||
status_code=200,
|
status_code=200,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/classification/{name}/dataset/{category}/delete",
|
||||||
|
dependencies=[Depends(require_role(["admin"]))],
|
||||||
|
)
|
||||||
|
def delete_classification_dataset_images(
|
||||||
|
request: Request, name: str, category: str, body: dict = None
|
||||||
|
):
|
||||||
|
config: FrigateConfig = request.app.frigate_config
|
||||||
|
|
||||||
|
if name not in config.classification.custom:
|
||||||
|
return JSONResponse(
|
||||||
|
content=(
|
||||||
|
{
|
||||||
|
"success": False,
|
||||||
|
"message": f"{name} is not a known classification model.",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
status_code=404,
|
||||||
|
)
|
||||||
|
|
||||||
|
json: dict[str, Any] = body or {}
|
||||||
|
list_of_ids = json.get("ids", "")
|
||||||
|
folder = os.path.join(
|
||||||
|
CLIPS_DIR, sanitize_filename(name), "dataset", sanitize_filename(category)
|
||||||
|
)
|
||||||
|
|
||||||
|
for id in list_of_ids:
|
||||||
|
file_path = os.path.join(folder, id)
|
||||||
|
|
||||||
|
if os.path.isfile(file_path):
|
||||||
|
os.unlink(file_path)
|
||||||
|
|
||||||
|
return JSONResponse(
|
||||||
|
content=({"success": True, "message": "Successfully deleted faces."}),
|
||||||
|
status_code=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/classification/{name}/train/delete",
|
||||||
|
dependencies=[Depends(require_role(["admin"]))],
|
||||||
|
)
|
||||||
|
def delete_classification_train_images(request: Request, name: str, body: dict = None):
|
||||||
|
config: FrigateConfig = request.app.frigate_config
|
||||||
|
|
||||||
|
if name not in config.classification.custom:
|
||||||
|
return JSONResponse(
|
||||||
|
content=(
|
||||||
|
{
|
||||||
|
"success": False,
|
||||||
|
"message": f"{name} is not a known classification model.",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
status_code=404,
|
||||||
|
)
|
||||||
|
|
||||||
|
json: dict[str, Any] = body or {}
|
||||||
|
list_of_ids = json.get("ids", "")
|
||||||
|
folder = os.path.join(CLIPS_DIR, sanitize_filename(name), "train")
|
||||||
|
|
||||||
|
for id in list_of_ids:
|
||||||
|
file_path = os.path.join(folder, id)
|
||||||
|
|
||||||
|
if os.path.isfile(file_path):
|
||||||
|
os.unlink(file_path)
|
||||||
|
|
||||||
|
return JSONResponse(
|
||||||
|
content=({"success": True, "message": "Successfully deleted faces."}),
|
||||||
|
status_code=200,
|
||||||
|
)
|
||||||
|
|||||||
@ -1,5 +1,33 @@
|
|||||||
{
|
{
|
||||||
"button": {
|
"button": {
|
||||||
"deleteClassificationAttempts": "Delete Classification Images"
|
"deleteClassificationAttempts": "Delete Classification Images",
|
||||||
}
|
"renameCategory": "Rename Category",
|
||||||
|
"deleteCategory": "Rename Category"
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
"success": {
|
||||||
|
"deletedCategory": "Deleted Category",
|
||||||
|
"deletedImage": "Deleted Images"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"deleteImageFailed": "Failed to delete: {{errorMessage}}",
|
||||||
|
"deleteCategoryFailed": "Failed to delete category: {{errorMessage}}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deleteCategory": {
|
||||||
|
"title": "Delete Category",
|
||||||
|
"desc": "Are you sure you want to delete the category {{name}}? This will permanently delete all associated images and require re-training the model."
|
||||||
|
},
|
||||||
|
"renameCategory": {
|
||||||
|
"title": "Rename Category",
|
||||||
|
"desc": "Enter a new name for {{name}}. You will be required to retrain the model for the name change to take affect."
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"invalidName": " \"invalidName\": \"Invalid name. Names can only include letters, numbers, spaces, apostrophes, underscores, and hyphens.\""
|
||||||
|
},
|
||||||
|
"train": {
|
||||||
|
"title": "Train",
|
||||||
|
"aria": "Select Train"
|
||||||
|
},
|
||||||
|
"categories": "Categories"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,6 +41,7 @@ export default function ModelSelectionView({
|
|||||||
key={config.name}
|
key={config.name}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-52 cursor-pointer flex-col gap-2 rounded-lg bg-card p-2 outline outline-[3px]",
|
"flex h-52 cursor-pointer flex-col gap-2 rounded-lg bg-card p-2 outline outline-[3px]",
|
||||||
|
"outline-transparent duration-500",
|
||||||
isMobile && "w-full",
|
isMobile && "w-full",
|
||||||
)}
|
)}
|
||||||
onClick={() => onClick(config)}
|
onClick={() => onClick(config)}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@ -29,23 +30,25 @@ import { useCallback, useMemo, useState } from "react";
|
|||||||
import { isMobile } from "react-device-detect";
|
import { isMobile } from "react-device-detect";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { LuPencil, LuTrash2 } from "react-icons/lu";
|
import { LuPencil, LuTrash2 } from "react-icons/lu";
|
||||||
|
import { toast } from "sonner";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
type ModelTrainingViewProps = {
|
type ModelTrainingViewProps = {
|
||||||
model: CustomClassificationModelConfig;
|
model: CustomClassificationModelConfig;
|
||||||
};
|
};
|
||||||
export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
|
export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
|
||||||
|
const { t } = useTranslation(["views/classificationModel"]);
|
||||||
const [page, setPage] = useState<string>("train");
|
const [page, setPage] = useState<string>("train");
|
||||||
const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100);
|
const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100);
|
||||||
|
|
||||||
// dataset
|
// dataset
|
||||||
|
|
||||||
const { data: trainImages } = useSWR<string[]>(
|
const { data: trainImages, mutate: refreshTrain } = useSWR<string[]>(
|
||||||
`classification/${model.name}/train`,
|
`classification/${model.name}/train`,
|
||||||
);
|
);
|
||||||
const { data: dataset } = useSWR<{ [id: string]: string[] }>(
|
const { data: dataset, mutate: refreshDataset } = useSWR<{
|
||||||
`classification/${model.name}/dataset`,
|
[id: string]: string[];
|
||||||
);
|
}>(`classification/${model.name}/dataset`);
|
||||||
|
|
||||||
// actions
|
// actions
|
||||||
|
|
||||||
@ -53,15 +56,75 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
|
|||||||
axios.post(`classification/${model.name}/train`);
|
axios.post(`classification/${model.name}/train`);
|
||||||
}, [model]);
|
}, [model]);
|
||||||
|
|
||||||
|
const onDelete = useCallback(
|
||||||
|
(ids: string[], isName: boolean = false) => {
|
||||||
|
const api =
|
||||||
|
pageToggle == "train"
|
||||||
|
? `/classification/${model.name}/train/delete`
|
||||||
|
: `/classification/${model.name}/dataset/${pageToggle}/delete`;
|
||||||
|
|
||||||
|
axios
|
||||||
|
.post(api, { ids })
|
||||||
|
.then((resp) => {
|
||||||
|
//setSelectedFaces([]);
|
||||||
|
|
||||||
|
if (resp.status == 200) {
|
||||||
|
if (isName) {
|
||||||
|
toast.success(
|
||||||
|
t("toast.success.deletedCategory", { count: ids.length }),
|
||||||
|
{
|
||||||
|
position: "top-center",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toast.success(
|
||||||
|
t("toast.success.deletedImage", { count: ids.length }),
|
||||||
|
{
|
||||||
|
position: "top-center",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageToggle == "train") {
|
||||||
|
refreshTrain();
|
||||||
|
} else {
|
||||||
|
refreshDataset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const errorMessage =
|
||||||
|
error.response?.data?.message ||
|
||||||
|
error.response?.data?.detail ||
|
||||||
|
"Unknown error";
|
||||||
|
if (isName) {
|
||||||
|
toast.error(
|
||||||
|
t("toast.error.deleteCategoryFailed", { errorMessage }),
|
||||||
|
{
|
||||||
|
position: "top-center",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toast.error(t("toast.error.deleteImageFailed", { errorMessage }), {
|
||||||
|
position: "top-center",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[pageToggle, model, refreshTrain, refreshDataset, t],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex size-full flex-col gap-2 overflow-hidden p-2">
|
<div className="flex size-full flex-col overflow-hidden p-2">
|
||||||
<div className="flex flex-row justify-between gap-2 align-middle">
|
<Toaster />
|
||||||
|
|
||||||
|
<div className="mb-2 flex flex-row justify-between gap-2 align-middle">
|
||||||
<LibrarySelector
|
<LibrarySelector
|
||||||
pageToggle={pageToggle}
|
pageToggle={pageToggle}
|
||||||
dataset={dataset || {}}
|
dataset={dataset || {}}
|
||||||
trainImages={trainImages || []}
|
trainImages={trainImages || []}
|
||||||
setPageToggle={setPageToggle}
|
setPageToggle={setPageToggle}
|
||||||
onDelete={() => {}}
|
onDelete={onDelete}
|
||||||
onRename={() => {}}
|
onRename={() => {}}
|
||||||
/>
|
/>
|
||||||
<Button onClick={trainModel}>Train Model</Button>
|
<Button onClick={trainModel}>Train Model</Button>
|
||||||
@ -72,14 +135,14 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
|
|||||||
trainImages={trainImages || []}
|
trainImages={trainImages || []}
|
||||||
selected={false}
|
selected={false}
|
||||||
onClickImages={() => {}}
|
onClickImages={() => {}}
|
||||||
onDelete={() => {}}
|
onDelete={onDelete}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<DatasetGrid
|
<DatasetGrid
|
||||||
modelName={model.name}
|
modelName={model.name}
|
||||||
categoryName={pageToggle}
|
categoryName={pageToggle}
|
||||||
images={dataset?.[pageToggle] || []}
|
images={dataset?.[pageToggle] || []}
|
||||||
onDelete={() => {}}
|
onDelete={onDelete}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -91,7 +154,7 @@ type LibrarySelectorProps = {
|
|||||||
dataset: { [id: string]: string[] };
|
dataset: { [id: string]: string[] };
|
||||||
trainImages: string[];
|
trainImages: string[];
|
||||||
setPageToggle: (toggle: string) => void;
|
setPageToggle: (toggle: string) => void;
|
||||||
onDelete: (name: string, ids: string[], isName: boolean) => void;
|
onDelete: (ids: string[], isName: boolean) => void;
|
||||||
onRename: (old_name: string, new_name: string) => void;
|
onRename: (old_name: string, new_name: string) => void;
|
||||||
};
|
};
|
||||||
function LibrarySelector({
|
function LibrarySelector({
|
||||||
@ -102,7 +165,7 @@ function LibrarySelector({
|
|||||||
onDelete,
|
onDelete,
|
||||||
onRename,
|
onRename,
|
||||||
}: LibrarySelectorProps) {
|
}: LibrarySelectorProps) {
|
||||||
const { t } = useTranslation(["views/faceLibrary"]);
|
const { t } = useTranslation(["views/classificationModel"]);
|
||||||
const [confirmDelete, setConfirmDelete] = useState<string | null>(null);
|
const [confirmDelete, setConfirmDelete] = useState<string | null>(null);
|
||||||
const [renameFace, setRenameFace] = useState<string | null>(null);
|
const [renameFace, setRenameFace] = useState<string | null>(null);
|
||||||
|
|
||||||
@ -111,7 +174,7 @@ function LibrarySelector({
|
|||||||
// Get all image IDs for this face
|
// Get all image IDs for this face
|
||||||
const imageIds = dataset?.[name] || [];
|
const imageIds = dataset?.[name] || [];
|
||||||
|
|
||||||
onDelete(name, imageIds, true);
|
onDelete(imageIds, true);
|
||||||
setPageToggle("train");
|
setPageToggle("train");
|
||||||
},
|
},
|
||||||
[dataset, onDelete, setPageToggle],
|
[dataset, onDelete, setPageToggle],
|
||||||
@ -132,9 +195,9 @@ function LibrarySelector({
|
|||||||
>
|
>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t("deleteFaceLibrary.title")}</DialogTitle>
|
<DialogTitle>{t("deleteCategory.title")}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{t("deleteFaceLibrary.desc", { name: confirmDelete })}
|
{t("deleteCategory.desc", { name: confirmDelete })}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
@ -159,8 +222,8 @@ function LibrarySelector({
|
|||||||
<TextEntryDialog
|
<TextEntryDialog
|
||||||
open={!!renameFace}
|
open={!!renameFace}
|
||||||
setOpen={handleSetOpen}
|
setOpen={handleSetOpen}
|
||||||
title={t("renameFace.title")}
|
title={t("renameCategory.title")}
|
||||||
description={t("renameFace.desc", { name: renameFace })}
|
description={t("renameCategory.desc", { name: renameFace })}
|
||||||
onSave={(newName) => {
|
onSave={(newName) => {
|
||||||
onRename(renameFace!, newName);
|
onRename(renameFace!, newName);
|
||||||
setRenameFace(null);
|
setRenameFace(null);
|
||||||
@ -203,7 +266,7 @@ function LibrarySelector({
|
|||||||
<>
|
<>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<div className="mb-1 ml-1.5 text-xs text-secondary-foreground">
|
<div className="mb-1 ml-1.5 text-xs text-secondary-foreground">
|
||||||
{t("collections")}
|
{t("categories")}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -237,7 +300,9 @@ function LibrarySelector({
|
|||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPortal>
|
<TooltipPortal>
|
||||||
<TooltipContent>{t("button.renameFace")}</TooltipContent>
|
<TooltipContent>
|
||||||
|
{t("button.renameCategory")}
|
||||||
|
</TooltipContent>
|
||||||
</TooltipPortal>
|
</TooltipPortal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@ -255,7 +320,9 @@ function LibrarySelector({
|
|||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPortal>
|
<TooltipPortal>
|
||||||
<TooltipContent>{t("button.deleteFace")}</TooltipContent>
|
<TooltipContent>
|
||||||
|
{t("button.deleteCategory")}
|
||||||
|
</TooltipContent>
|
||||||
</TooltipPortal>
|
</TooltipPortal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
@ -271,7 +338,7 @@ type DatasetGridProps = {
|
|||||||
modelName: string;
|
modelName: string;
|
||||||
categoryName: string;
|
categoryName: string;
|
||||||
images: string[];
|
images: string[];
|
||||||
onDelete: (modelName: string, categoryName: string, ids: string[]) => void;
|
onDelete: (ids: string[]) => void;
|
||||||
};
|
};
|
||||||
function DatasetGrid({
|
function DatasetGrid({
|
||||||
modelName,
|
modelName,
|
||||||
@ -314,7 +381,7 @@ function DatasetGrid({
|
|||||||
className="size-5 cursor-pointer text-primary-variant hover:text-primary"
|
className="size-5 cursor-pointer text-primary-variant hover:text-primary"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onDelete(modelName, categoryName, [image]);
|
onDelete([image]);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
@ -336,7 +403,7 @@ type TrainGridProps = {
|
|||||||
trainImages: string[];
|
trainImages: string[];
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
onClickImages: (images: string[], ctrl: boolean) => void;
|
onClickImages: (images: string[], ctrl: boolean) => void;
|
||||||
onDelete: (name: string, ids: string[]) => void;
|
onDelete: (ids: string[]) => void;
|
||||||
};
|
};
|
||||||
function TrainGrid({
|
function TrainGrid({
|
||||||
model,
|
model,
|
||||||
@ -401,7 +468,7 @@ function TrainGrid({
|
|||||||
className="size-5 cursor-pointer text-primary-variant hover:text-primary"
|
className="size-5 cursor-pointer text-primary-variant hover:text-primary"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onDelete("train", [data.raw]);
|
onDelete([data.raw]);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user