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 useOptimisticState from "@/hooks/use-optimistic-state";
import { cn } from "@/lib/utils";
import { Event } from "@/types/event";
import { FaceLibraryData, RecognizedFaceData } from "@/types/face";
import { FaceRecognitionConfig, FrigateConfig } from "@/types/frigateConfig";
import axios from "axios";
@ -395,6 +396,41 @@ function TrainingGrid({
// 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 formattedDate = useFormattedTimestamp(
@ -405,6 +441,10 @@ function TrainingGrid({
config?.ui.timezone,
);
if (!events) {
return null;
}
return (
<>
<Dialog
@ -446,30 +486,49 @@ function TrainingGrid({
</Dialog>
<div className="scrollbar-container flex flex-wrap gap-2 overflow-y-scroll p-1">
{attemptImages.map((image: string) => (
<FaceAttempt
key={image}
image={image}
faceNames={faceNames}
recognitionConfig={config.face_recognition}
selected={selectedFaces.includes(image)}
onClick={(data, meta) => {
if (meta) {
onClickFace(image, meta);
} else {
setSelectedEvent(data);
}
}}
onRefresh={onRefresh}
/>
))}
{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
key={data.timestamp}
data={data}
faceNames={faceNames}
recognitionConfig={config.face_recognition}
selected={selectedFaces.includes(data.filename)}
onClick={(data, meta) => {
if (meta) {
onClickFace(data.filename, meta);
} else {
setSelectedEvent(data);
}
}}
onRefresh={onRefresh}
/>
))}
</div>
</div>
);
})}
</div>
</>
);
}
type FaceAttemptProps = {
image: string;
data: RecognizedFaceData;
faceNames: string[];
recognitionConfig: FaceRecognitionConfig;
selected: boolean;
@ -477,7 +536,7 @@ type FaceAttemptProps = {
onRefresh: () => void;
};
function FaceAttempt({
image,
data,
faceNames,
recognitionConfig,
selected,
@ -485,16 +544,6 @@ function FaceAttempt({
onRefresh,
}: FaceAttemptProps) {
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(() => {
if (data.score >= recognitionConfig.recognition_threshold) {
@ -519,7 +568,9 @@ function FaceAttempt({
const onTrainAttempt = useCallback(
(trainName: string) => {
axios
.post(`/faces/train/${trainName}/classify`, { training_file: image })
.post(`/faces/train/${trainName}/classify`, {
training_file: data.eventId,
})
.then((resp) => {
if (resp.status == 200) {
toast.success(t("toast.success.trainedFace"), {
@ -538,12 +589,12 @@ function FaceAttempt({
});
});
},
[image, onRefresh, t],
[data, onRefresh, t],
);
const onReprocess = useCallback(() => {
axios
.post(`/faces/reprocess`, { training_file: image })
.post(`/faces/reprocess`, { training_file: data.filename })
.then((resp) => {
if (resp.status == 200) {
toast.success(t("toast.success.updatedFaceScore"), {
@ -561,7 +612,7 @@ function FaceAttempt({
position: "top-center",
});
});
}, [image, onRefresh, t]);
}, [data, onRefresh, t]);
return (
<div
@ -576,7 +627,7 @@ function FaceAttempt({
<img
ref={imgRef}
className="size-44"
src={`${baseUrl}clips/faces/train/${image}`}
src={`${baseUrl}clips/faces/train/${data.filename}`}
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">

View File

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