Compare commits

..

No commits in common. "8631dcfe8bdc65972f9cbe126eddeba8eebb40bb" and "0bcabf0731b48f1f993d1013835b54fa30509c17" have entirely different histories.

4 changed files with 156 additions and 142 deletions

View File

@ -197,9 +197,7 @@ class FaceRecognitionConfig(FrigateBaseModel):
title="Min face recognitions for the sub label to be applied to the person object.", title="Min face recognitions for the sub label to be applied to the person object.",
) )
save_attempts: int = Field( save_attempts: int = Field(
default=200, default=200, ge=0, title="Number of face attempts to save in the recent recognitions tab."
ge=0,
title="Number of face attempts to save in the recent recognitions tab.",
) )
blur_confidence_filter: bool = Field( blur_confidence_filter: bool = Field(
default=True, title="Apply blur quality filter to face confidence." default=True, title="Apply blur quality filter to face confidence."

View File

@ -34,7 +34,6 @@ import {
} from "../mobile/MobilePage"; } from "../mobile/MobilePage";
type ClassificationCardProps = { type ClassificationCardProps = {
className?: string;
imgClassName?: string; imgClassName?: string;
data: ClassificationItemData; data: ClassificationItemData;
threshold?: ClassificationThreshold; threshold?: ClassificationThreshold;
@ -50,7 +49,6 @@ export const ClassificationCard = forwardRef<
ClassificationCardProps ClassificationCardProps
>(function ClassificationCard( >(function ClassificationCard(
{ {
className,
imgClassName, imgClassName,
data, data,
threshold, threshold,
@ -100,8 +98,8 @@ export const ClassificationCard = forwardRef<
<div <div
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex size-full cursor-pointer flex-col overflow-hidden rounded-lg outline outline-[3px]", "relative flex cursor-pointer flex-col overflow-hidden rounded-lg outline outline-[3px]",
className, isMobile ? "!size-full" : "size-48",
selected selected
? "shadow-selected outline-selected" ? "shadow-selected outline-selected"
: "outline-transparent duration-500", : "outline-transparent duration-500",
@ -213,13 +211,13 @@ export function GroupedClassificationCard({
}); });
if (!best) { if (!best) {
return group.at(-1); return group[0];
} }
const bestTyped: ClassificationItemData = best; const bestTyped: ClassificationItemData = best;
return { return {
...bestTyped, ...bestTyped,
name: event ? (event.sub_label ?? "") : bestTyped.name, name: event?.sub_label || bestTyped.name,
score: event?.data?.sub_label_score || bestTyped.score, score: event?.data?.sub_label_score || bestTyped.score,
}; };
}, [group, event]); }, [group, event]);
@ -289,18 +287,39 @@ export function GroupedClassificationCard({
<Content <Content
className={cn( className={cn(
"", "",
isDesktop && "min-w-[50%] max-w-[65%]", isDesktop && "w-auto max-w-[85%]",
isMobile && "flex flex-col", isMobile && "flex flex-col",
)} )}
onOpenAutoFocus={(e) => e.preventDefault()} onOpenAutoFocus={(e) => e.preventDefault()}
> >
<> <>
<Header {isDesktop && (
className={cn( <div className="absolute right-10 top-4 flex flex-row justify-between">
"mx-2 flex flex-row items-center gap-4", {event && (
isMobile && "flex-shrink-0", <Tooltip>
)} <TooltipTrigger asChild>
> <div
className="cursor-pointer"
tabIndex={-1}
onClick={() => {
navigate(`/explore?event_id=${event.id}`);
}}
>
<LuSearch className="size-4 text-secondary-foreground" />
</div>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent>
{t("details.item.button.viewInExplore", {
ns: "views/explore",
})}
</TooltipContent>
</TooltipPortal>
</Tooltip>
)}
</div>
)}
<Header className={cn("mx-2", isMobile && "flex-shrink-0")}>
<div> <div>
<ContentTitle <ContentTitle
className={cn( className={cn(
@ -330,57 +349,43 @@ export function GroupedClassificationCard({
)} )}
</ContentDescription> </ContentDescription>
</div> </div>
{isDesktop && (
<div className="flex flex-row justify-between">
{event && (
<Tooltip>
<TooltipTrigger asChild>
<div
className="cursor-pointer"
tabIndex={-1}
onClick={() => {
navigate(`/explore?event_id=${event.id}`);
}}
>
<LuSearch className="size-4 text-secondary-foreground" />
</div>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent>
{t("details.item.button.viewInExplore", {
ns: "views/explore",
})}
</TooltipContent>
</TooltipPortal>
</Tooltip>
)}
</div>
)}
</Header> </Header>
<div <div
className={cn( className={cn(
"grid w-full auto-rows-min grid-cols-2 gap-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-6 2xl:grid-cols-8", "flex cursor-pointer flex-col gap-2 rounded-lg",
isDesktop && "p-2", isDesktop && "p-2",
isMobile && "scrollbar-container flex-1 overflow-y-auto", isMobile && "scrollbar-container w-full flex-1 overflow-y-auto",
)} )}
> >
{group.map((data: ClassificationItemData) => ( <div
<div key={data.filename} className="aspect-square w-full"> className={cn(
<ClassificationCard "gap-2",
data={data} isDesktop
threshold={threshold} ? "flex flex-row flex-wrap"
selected={false} : "grid grid-cols-2 justify-items-center gap-2 px-2 sm:grid-cols-5 lg:grid-cols-6",
i18nLibrary={i18nLibrary} )}
onClick={(data, meta) => { >
if (meta || selectedItems.length > 0) { {group.map((data: ClassificationItemData) => (
onClick(data); <div
} key={data.filename}
}} className={cn(isMobile && "aspect-square size-full")}
> >
{children?.(data)} <ClassificationCard
</ClassificationCard> data={data}
</div> threshold={threshold}
))} selected={false}
i18nLibrary={i18nLibrary}
onClick={(data, meta) => {
if (meta || selectedItems.length > 0) {
onClick(data);
}
}}
>
{children?.(data)}
</ClassificationCard>
</div>
))}
</div>
</div> </div>
</> </>
</Content> </Content>

View File

@ -51,7 +51,7 @@ import {
useRef, useRef,
useState, useState,
} from "react"; } from "react";
import { isDesktop } from "react-device-detect"; import { isDesktop, isMobile, isMobileOnly } from "react-device-detect";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { import {
LuFolderCheck, LuFolderCheck,
@ -695,13 +695,16 @@ function TrainingGrid({
<div <div
ref={contentRef} ref={contentRef}
className={cn( className={cn(
"scrollbar-container grid grid-cols-2 gap-3 overflow-y-scroll p-1 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-10 3xl:grid-cols-12", "scrollbar-container gap-3 overflow-y-scroll p-1",
isMobile
? "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8"
: "flex flex-wrap",
)} )}
> >
{Object.entries(faceGroups).map(([key, group]) => { {Object.entries(faceGroups).map(([key, group]) => {
const event = events?.find((ev) => ev.id == key); const event = events?.find((ev) => ev.id == key);
return ( return (
<div key={key} className="aspect-square w-full"> <div key={key} className={cn(isMobile && "aspect-square size-full")}>
<FaceAttemptGroup <FaceAttemptGroup
config={config} config={config}
group={group} group={group}
@ -858,12 +861,12 @@ function FaceAttemptGroup({
faceNames={faceNames} faceNames={faceNames}
onTrainAttempt={(name) => onTrainAttempt(data, name)} onTrainAttempt={(name) => onTrainAttempt(data, name)}
> >
<AddFaceIcon className="size-7 cursor-pointer p-1 text-gray-200 hover:rounded-full hover:bg-primary-foreground/40" /> <AddFaceIcon className="size-7 cursor-pointer p-1 text-gray-200 hover:rounded-full hover:bg-primary-foreground" />
</FaceSelectionDialog> </FaceSelectionDialog>
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger>
<LuRefreshCw <LuRefreshCw
className="size-7 cursor-pointer p-1 text-gray-200 hover:rounded-full hover:bg-primary-foreground/40" className="size-7 cursor-pointer p-1 text-gray-200 hover:rounded-full hover:bg-primary-foreground"
onClick={() => onReprocess(data)} onClick={() => onReprocess(data)}
/> />
</TooltipTrigger> </TooltipTrigger>
@ -911,35 +914,35 @@ function FaceGrid({
<div <div
ref={contentRef} ref={contentRef}
className={cn( className={cn(
"scrollbar-container grid grid-cols-2 gap-2 overflow-y-scroll p-1 md:grid-cols-4 xl:grid-cols-8 2xl:grid-cols-10 3xl:grid-cols-12", "scrollbar-container gap-2 overflow-y-scroll p-1",
isDesktop ? "flex flex-wrap" : "grid grid-cols-2 md:grid-cols-4",
)} )}
> >
{sortedFaces.map((image: string) => ( {sortedFaces.map((image: string) => (
<div key={image} className="aspect-square w-full"> <ClassificationCard
<ClassificationCard key={image}
data={{ data={{
name: pageToggle, name: pageToggle,
filename: image, filename: image,
filepath: `clips/faces/${pageToggle}/${image}`, filepath: `clips/faces/${pageToggle}/${image}`,
}} }}
selected={selectedFaces.includes(image)} selected={selectedFaces.includes(image)}
i18nLibrary="views/faceLibrary" i18nLibrary="views/faceLibrary"
onClick={(data, meta) => onClickFaces([data.filename], meta)} onClick={(data, meta) => onClickFaces([data.filename], meta)}
> >
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger>
<LuTrash2 <LuTrash2
className="size-5 cursor-pointer text-gray-200 hover:text-danger" className="size-5 cursor-pointer text-gray-200 hover:text-danger"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onDelete(pageToggle, [image]); onDelete(pageToggle, [image]);
}} }}
/> />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>{t("button.deleteFaceAttempts")}</TooltipContent> <TooltipContent>{t("button.deleteFaceAttempts")}</TooltipContent>
</Tooltip> </Tooltip>
</ClassificationCard> </ClassificationCard>
</div>
))} ))}
</div> </div>
); );

View File

@ -44,7 +44,7 @@ import {
useRef, useRef,
useState, useState,
} from "react"; } from "react";
import { isDesktop } from "react-device-detect"; import { isDesktop, isMobile, isMobileOnly } from "react-device-detect";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { LuPencil, LuTrash2 } from "react-icons/lu"; import { LuPencil, LuTrash2 } from "react-icons/lu";
import { toast } from "sonner"; import { toast } from "sonner";
@ -632,36 +632,36 @@ function DatasetGrid({
return ( return (
<div <div
ref={contentRef} ref={contentRef}
className="scrollbar-container grid grid-cols-2 gap-2 overflow-y-scroll p-1 md:grid-cols-4 xl:grid-cols-8 2xl:grid-cols-10 3xl:grid-cols-12" className="scrollbar-container flex flex-wrap gap-2 overflow-y-auto p-2"
> >
{classData.map((image) => ( {classData.map((image) => (
<div key={image} className="aspect-square w-full"> <ClassificationCard
<ClassificationCard key={image}
data={{ imgClassName="size-auto"
filename: image, data={{
filepath: `clips/${modelName}/dataset/${categoryName}/${image}`, filename: image,
name: "", filepath: `clips/${modelName}/dataset/${categoryName}/${image}`,
}} name: "",
selected={selectedImages.includes(image)} }}
i18nLibrary="views/classificationModel" selected={selectedImages.includes(image)}
onClick={(data, _) => onClickImages([data.filename], true)} i18nLibrary="views/classificationModel"
> onClick={(data, _) => onClickImages([data.filename], true)}
<Tooltip> >
<TooltipTrigger> <Tooltip>
<LuTrash2 <TooltipTrigger>
className="size-5 cursor-pointer text-primary-variant hover:text-danger" <LuTrash2
onClick={(e) => { className="size-5 cursor-pointer text-primary-variant hover:text-danger"
e.stopPropagation(); onClick={(e) => {
onDelete([image]); e.stopPropagation();
}} onDelete([image]);
/> }}
</TooltipTrigger> />
<TooltipContent> </TooltipTrigger>
{t("button.deleteClassificationAttempts")} <TooltipContent>
</TooltipContent> {t("button.deleteClassificationAttempts")}
</Tooltip> </TooltipContent>
</ClassificationCard> </Tooltip>
</div> </ClassificationCard>
))} ))}
</div> </div>
); );
@ -791,29 +791,30 @@ function StateTrainGrid({
<div <div
ref={contentRef} ref={contentRef}
className={cn( className={cn(
"scrollbar-container grid grid-cols-2 gap-3 overflow-y-scroll p-1 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-10 3xl:grid-cols-12", "scrollbar-container flex flex-wrap gap-3 overflow-y-auto p-2",
isMobile && "justify-center",
)} )}
> >
{trainData?.map((data) => ( {trainData?.map((data) => (
<div key={data.filename} className="aspect-square w-full"> <ClassificationCard
<ClassificationCard key={data.filename}
data={data} imgClassName="size-auto"
threshold={threshold} data={data}
selected={selectedImages.includes(data.filename)} threshold={threshold}
i18nLibrary="views/classificationModel" selected={selectedImages.includes(data.filename)}
showArea={false} i18nLibrary="views/classificationModel"
onClick={(data, meta) => onClickImages([data.filename], meta)} showArea={false}
onClick={(data, meta) => onClickImages([data.filename], meta)}
>
<ClassificationSelectionDialog
classes={classes}
modelName={model.name}
image={data.filename}
onRefresh={onRefresh}
> >
<ClassificationSelectionDialog <TbCategoryPlus className="size-7 cursor-pointer p-1 text-gray-200 hover:rounded-full hover:bg-primary-foreground" />
classes={classes} </ClassificationSelectionDialog>
modelName={model.name} </ClassificationCard>
image={data.filename}
onRefresh={onRefresh}
>
<TbCategoryPlus className="size-7 cursor-pointer p-1 text-gray-200 hover:rounded-full hover:bg-primary-foreground/40" />
</ClassificationSelectionDialog>
</ClassificationCard>
</div>
))} ))}
</div> </div>
); );
@ -927,14 +928,21 @@ function ObjectTrainGrid({
<div <div
ref={contentRef} ref={contentRef}
className={cn( className={cn(
"scrollbar-container grid grid-cols-2 gap-3 overflow-y-scroll p-1 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-10 3xl:grid-cols-12", "scrollbar-container gap-3 overflow-y-scroll p-1",
isMobile
? "grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8"
: "flex flex-wrap",
)} )}
> >
{Object.entries(groups).map(([key, group]) => { {Object.entries(groups).map(([key, group]) => {
const event = events?.find((ev) => ev.id == key); const event = events?.find((ev) => ev.id == key);
return ( return (
<div key={key} className="aspect-square w-full"> <div
key={key}
className={cn(isMobile && "aspect-square size-full")}
>
<GroupedClassificationCard <GroupedClassificationCard
key={key}
group={group} group={group}
event={event} event={event}
threshold={threshold} threshold={threshold}
@ -957,7 +965,7 @@ function ObjectTrainGrid({
image={data.filename} image={data.filename}
onRefresh={onRefresh} onRefresh={onRefresh}
> >
<TbCategoryPlus className="size-7 cursor-pointer p-1 text-gray-200 hover:rounded-full hover:bg-primary-foreground/40" /> <TbCategoryPlus className="size-7 cursor-pointer p-1 text-gray-200 hover:rounded-full hover:bg-primary-foreground" />
</ClassificationSelectionDialog> </ClassificationSelectionDialog>
</> </>
)} )}