From 28f564540c57a510e1563e8df8e0cefd9da4c548 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 3 Jun 2025 09:39:41 -0600 Subject: [PATCH] Implement model training screen with dataset selection --- web/src/pages/ClassificationModel.tsx | 1 - .../classification/ModelSelectionView.tsx | 4 +- .../classification/ModelTrainingView.tsx | 235 +++++++++++++++++- 3 files changed, 232 insertions(+), 8 deletions(-) diff --git a/web/src/pages/ClassificationModel.tsx b/web/src/pages/ClassificationModel.tsx index 833e2e6db..c37d0b454 100644 --- a/web/src/pages/ClassificationModel.tsx +++ b/web/src/pages/ClassificationModel.tsx @@ -2,7 +2,6 @@ import { useOverlayState } from "@/hooks/use-overlay-state"; import { CustomClassificationModelConfig } from "@/types/frigateConfig"; import ModelSelectionView from "@/views/classification/ModelSelectionView"; import ModelTrainingView from "@/views/classification/ModelTrainingView"; -import { useState } from "react"; export default function ClassificationModelPage() { // training diff --git a/web/src/views/classification/ModelSelectionView.tsx b/web/src/views/classification/ModelSelectionView.tsx index 91d63194b..502a53251 100644 --- a/web/src/views/classification/ModelSelectionView.tsx +++ b/web/src/views/classification/ModelSelectionView.tsx @@ -35,11 +35,11 @@ export default function ModelSelectionView({ } return ( -
+
{classificationConfigs.map((config) => (
("train"); + const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100); + + // dataset + + const { data: trainImages } = useSWR(`classification/${model.name}/train`); + const { data: dataset } = useSWR(`classification/${model.name}/dataset`); + + // actions + + const trainModel = useCallback(() => { + axios.post(`classification/${model.name}/train`); + }, [model]); + return (
- - {model.name} ({model.state_config != null ? "State" : "Object"}{" "} - Classification) - +
+ {}} + onRename={() => {}} + /> + +
); } + +type LibrarySelectorProps = { + pageToggle: string | undefined; + dataset: { [id: string]: string[] }; + trainImages: string[]; + setPageToggle: (toggle: string) => void; + onDelete: (name: string, ids: string[], isName: boolean) => void; + onRename: (old_name: string, new_name: string) => void; +}; +function LibrarySelector({ + pageToggle, + dataset, + trainImages, + setPageToggle, + onDelete, + onRename, +}: LibrarySelectorProps) { + const { t } = useTranslation(["views/faceLibrary"]); + const [confirmDelete, setConfirmDelete] = useState(null); + const [renameFace, setRenameFace] = useState(null); + + const handleDeleteFace = useCallback( + (name: string) => { + // Get all image IDs for this face + const imageIds = dataset?.[name] || []; + + onDelete(name, imageIds, true); + setPageToggle("train"); + }, + [dataset, onDelete, setPageToggle], + ); + + const handleSetOpen = useCallback( + (open: boolean) => { + setRenameFace(open ? renameFace : null); + }, + [renameFace], + ); + + return ( + <> + !open && setConfirmDelete(null)} + > + + + {t("deleteFaceLibrary.title")} + + {t("deleteFaceLibrary.desc", { name: confirmDelete })} + + +
+ + +
+
+
+ + { + onRename(renameFace!, newName); + setRenameFace(null); + }} + defaultValue={renameFace || ""} + regexPattern={/^[\p{L}\p{N}\s'_-]{1,50}$/u} + regexErrorMessage={t("description.invalidName")} + /> + + + + + + + setPageToggle("train")} + > +
{t("train.title")}
+
+ ({trainImages.length}) +
+
+ {trainImages.length > 0 && Object.keys(dataset).length > 0 && ( + <> + +
+ {t("collections")} +
+ + )} + {Object.keys(dataset).map((id) => ( + +
setPageToggle(id)} + > + {id} + + ({dataset?.[id].length}) + +
+
+ + + + + + {t("button.renameFace")} + + + + + + + + {t("button.deleteFace")} + + +
+
+ ))} +
+
+ + ); +}