Section faces by event id

This commit is contained in:
Nicolas Mowen 2025-03-27 15:10:31 -06:00
parent 3f1b4438e4
commit c89bb12a4f
2 changed files with 86 additions and 34 deletions

View File

@ -32,6 +32,7 @@ import { useFormattedTimestamp } from "@/hooks/use-date-utils";
import useKeyboardListener from "@/hooks/use-keyboard-listener"; import useKeyboardListener from "@/hooks/use-keyboard-listener";
import useOptimisticState from "@/hooks/use-optimistic-state"; import useOptimisticState from "@/hooks/use-optimistic-state";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Event } from "@/types/event";
import { FaceLibraryData, RecognizedFaceData } from "@/types/face"; import { FaceLibraryData, RecognizedFaceData } from "@/types/face";
import { FaceRecognitionConfig, FrigateConfig } from "@/types/frigateConfig"; import { FaceRecognitionConfig, FrigateConfig } from "@/types/frigateConfig";
import axios from "axios"; import axios from "axios";
@ -395,6 +396,41 @@ function TrainingGrid({
// face data // face data
const faceGroups = useMemo(() => {
const groups: { [eventId: string]: RecognizedFaceData[] } = {};
attemptImages.forEach((image) => {
const parts = image.split("-");
const data = {
filename: image,
timestamp: Number.parseFloat(parts[0]),
eventId: `${parts[0]}-${parts[1]}`,
name: parts[2],
score: Number.parseFloat(parts[3]),
};
if (groups[data.eventId]) {
groups[data.eventId].push(data);
} else {
groups[data.eventId] = [data];
}
});
return groups;
}, [attemptImages]);
const eventIdsQuery = useMemo(
() => Object.keys(faceGroups).join(","),
[faceGroups],
);
const { data: events } = useSWR<Event[]>([
"event_ids",
{ ids: eventIdsQuery },
]);
// selection
const [selectedEvent, setSelectedEvent] = useState<RecognizedFaceData>(); const [selectedEvent, setSelectedEvent] = useState<RecognizedFaceData>();
const formattedDate = useFormattedTimestamp( const formattedDate = useFormattedTimestamp(
@ -405,6 +441,10 @@ function TrainingGrid({
config?.ui.timezone, config?.ui.timezone,
); );
if (!events) {
return null;
}
return ( return (
<> <>
<Dialog <Dialog
@ -446,16 +486,31 @@ function TrainingGrid({
</Dialog> </Dialog>
<div className="scrollbar-container flex flex-wrap gap-2 overflow-y-scroll p-1"> <div className="scrollbar-container flex flex-wrap gap-2 overflow-y-scroll p-1">
{attemptImages.map((image: string) => ( {Object.entries(faceGroups).map(([key, group]) => {
const event = events.find((ev) => ev.id == key);
if (!event) {
return null;
}
return (
<div className="flex flex-col gap-2 rounded-lg bg-card p-4">
<div className="capitalize">
{event.label} ({Math.round(event.data.top_score * 100)}%){" "}
{event.sub_label &&
`${event.sub_label} (${Math.round((event.data.sub_label_score || 0) * 100)}%)`}
</div>
<div className="flex flex-row">
{group.map((data: RecognizedFaceData) => (
<FaceAttempt <FaceAttempt
key={image} key={data.timestamp}
image={image} data={data}
faceNames={faceNames} faceNames={faceNames}
recognitionConfig={config.face_recognition} recognitionConfig={config.face_recognition}
selected={selectedFaces.includes(image)} selected={selectedFaces.includes(data.filename)}
onClick={(data, meta) => { onClick={(data, meta) => {
if (meta) { if (meta) {
onClickFace(image, meta); onClickFace(data.filename, meta);
} else { } else {
setSelectedEvent(data); setSelectedEvent(data);
} }
@ -464,12 +519,16 @@ function TrainingGrid({
/> />
))} ))}
</div> </div>
</div>
);
})}
</div>
</> </>
); );
} }
type FaceAttemptProps = { type FaceAttemptProps = {
image: string; data: RecognizedFaceData;
faceNames: string[]; faceNames: string[];
recognitionConfig: FaceRecognitionConfig; recognitionConfig: FaceRecognitionConfig;
selected: boolean; selected: boolean;
@ -477,7 +536,7 @@ type FaceAttemptProps = {
onRefresh: () => void; onRefresh: () => void;
}; };
function FaceAttempt({ function FaceAttempt({
image, data,
faceNames, faceNames,
recognitionConfig, recognitionConfig,
selected, selected,
@ -485,16 +544,6 @@ function FaceAttempt({
onRefresh, onRefresh,
}: FaceAttemptProps) { }: FaceAttemptProps) {
const { t } = useTranslation(["views/faceLibrary"]); const { t } = useTranslation(["views/faceLibrary"]);
const data = useMemo<RecognizedFaceData>(() => {
const parts = image.split("-");
return {
timestamp: Number.parseFloat(parts[0]),
eventId: `${parts[0]}-${parts[1]}`,
name: parts[2],
score: Number.parseFloat(parts[3]),
};
}, [image]);
const scoreStatus = useMemo(() => { const scoreStatus = useMemo(() => {
if (data.score >= recognitionConfig.recognition_threshold) { if (data.score >= recognitionConfig.recognition_threshold) {
@ -519,7 +568,9 @@ function FaceAttempt({
const onTrainAttempt = useCallback( const onTrainAttempt = useCallback(
(trainName: string) => { (trainName: string) => {
axios axios
.post(`/faces/train/${trainName}/classify`, { training_file: image }) .post(`/faces/train/${trainName}/classify`, {
training_file: data.eventId,
})
.then((resp) => { .then((resp) => {
if (resp.status == 200) { if (resp.status == 200) {
toast.success(t("toast.success.trainedFace"), { toast.success(t("toast.success.trainedFace"), {
@ -538,12 +589,12 @@ function FaceAttempt({
}); });
}); });
}, },
[image, onRefresh, t], [data, onRefresh, t],
); );
const onReprocess = useCallback(() => { const onReprocess = useCallback(() => {
axios axios
.post(`/faces/reprocess`, { training_file: image }) .post(`/faces/reprocess`, { training_file: data.filename })
.then((resp) => { .then((resp) => {
if (resp.status == 200) { if (resp.status == 200) {
toast.success(t("toast.success.updatedFaceScore"), { toast.success(t("toast.success.updatedFaceScore"), {
@ -561,7 +612,7 @@ function FaceAttempt({
position: "top-center", position: "top-center",
}); });
}); });
}, [image, onRefresh, t]); }, [data, onRefresh, t]);
return ( return (
<div <div
@ -576,7 +627,7 @@ function FaceAttempt({
<img <img
ref={imgRef} ref={imgRef}
className="size-44" className="size-44"
src={`${baseUrl}clips/faces/train/${image}`} src={`${baseUrl}clips/faces/train/${data.filename}`}
onClick={(e) => onClick(data, e.metaKey || e.ctrlKey)} onClick={(e) => onClick(data, e.metaKey || e.ctrlKey)}
/> />
<div className="absolute bottom-1 right-1 z-10 rounded-lg bg-black/50 px-2 py-1 text-xs text-white"> <div className="absolute bottom-1 right-1 z-10 rounded-lg bg-black/50 px-2 py-1 text-xs text-white">

View File

@ -3,6 +3,7 @@ export type FaceLibraryData = {
}; };
export type RecognizedFaceData = { export type RecognizedFaceData = {
filename: string;
timestamp: number; timestamp: number;
eventId: string; eventId: string;
name: string; name: string;