mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-16 01:56:43 +03:00
Compare commits
8 Commits
0bcabf0731
...
8631dcfe8b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8631dcfe8b | ||
|
|
81d184936f | ||
|
|
8f92ebbe0e | ||
|
|
7e738c6538 | ||
|
|
10e9080093 | ||
|
|
fd7475ac34 | ||
|
|
90a5f3a63e | ||
|
|
e246d84ddd |
@ -197,7 +197,9 @@ class FaceRecognitionConfig(FrigateBaseModel):
|
||||
title="Min face recognitions for the sub label to be applied to the person object.",
|
||||
)
|
||||
save_attempts: int = Field(
|
||||
default=200, ge=0, title="Number of face attempts to save in the recent recognitions tab."
|
||||
default=200,
|
||||
ge=0,
|
||||
title="Number of face attempts to save in the recent recognitions tab.",
|
||||
)
|
||||
blur_confidence_filter: bool = Field(
|
||||
default=True, title="Apply blur quality filter to face confidence."
|
||||
|
||||
@ -34,6 +34,7 @@ import {
|
||||
} from "../mobile/MobilePage";
|
||||
|
||||
type ClassificationCardProps = {
|
||||
className?: string;
|
||||
imgClassName?: string;
|
||||
data: ClassificationItemData;
|
||||
threshold?: ClassificationThreshold;
|
||||
@ -49,6 +50,7 @@ export const ClassificationCard = forwardRef<
|
||||
ClassificationCardProps
|
||||
>(function ClassificationCard(
|
||||
{
|
||||
className,
|
||||
imgClassName,
|
||||
data,
|
||||
threshold,
|
||||
@ -98,8 +100,8 @@ export const ClassificationCard = forwardRef<
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-pointer flex-col overflow-hidden rounded-lg outline outline-[3px]",
|
||||
isMobile ? "!size-full" : "size-48",
|
||||
"relative flex size-full cursor-pointer flex-col overflow-hidden rounded-lg outline outline-[3px]",
|
||||
className,
|
||||
selected
|
||||
? "shadow-selected outline-selected"
|
||||
: "outline-transparent duration-500",
|
||||
@ -211,13 +213,13 @@ export function GroupedClassificationCard({
|
||||
});
|
||||
|
||||
if (!best) {
|
||||
return group[0];
|
||||
return group.at(-1);
|
||||
}
|
||||
|
||||
const bestTyped: ClassificationItemData = best;
|
||||
return {
|
||||
...bestTyped,
|
||||
name: event?.sub_label || bestTyped.name,
|
||||
name: event ? (event.sub_label ?? "") : bestTyped.name,
|
||||
score: event?.data?.sub_label_score || bestTyped.score,
|
||||
};
|
||||
}, [group, event]);
|
||||
@ -287,39 +289,18 @@ export function GroupedClassificationCard({
|
||||
<Content
|
||||
className={cn(
|
||||
"",
|
||||
isDesktop && "w-auto max-w-[85%]",
|
||||
isDesktop && "min-w-[50%] max-w-[65%]",
|
||||
isMobile && "flex flex-col",
|
||||
)}
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<>
|
||||
{isDesktop && (
|
||||
<div className="absolute right-10 top-4 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 className={cn("mx-2", isMobile && "flex-shrink-0")}>
|
||||
<Header
|
||||
className={cn(
|
||||
"mx-2 flex flex-row items-center gap-4",
|
||||
isMobile && "flex-shrink-0",
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<ContentTitle
|
||||
className={cn(
|
||||
@ -349,43 +330,57 @@ export function GroupedClassificationCard({
|
||||
)}
|
||||
</ContentDescription>
|
||||
</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>
|
||||
<div
|
||||
className={cn(
|
||||
"flex cursor-pointer flex-col gap-2 rounded-lg",
|
||||
"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",
|
||||
isDesktop && "p-2",
|
||||
isMobile && "scrollbar-container w-full flex-1 overflow-y-auto",
|
||||
isMobile && "scrollbar-container flex-1 overflow-y-auto",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"gap-2",
|
||||
isDesktop
|
||||
? "flex flex-row flex-wrap"
|
||||
: "grid grid-cols-2 justify-items-center gap-2 px-2 sm:grid-cols-5 lg:grid-cols-6",
|
||||
)}
|
||||
>
|
||||
{group.map((data: ClassificationItemData) => (
|
||||
<div
|
||||
key={data.filename}
|
||||
className={cn(isMobile && "aspect-square size-full")}
|
||||
{group.map((data: ClassificationItemData) => (
|
||||
<div key={data.filename} className="aspect-square w-full">
|
||||
<ClassificationCard
|
||||
data={data}
|
||||
threshold={threshold}
|
||||
selected={false}
|
||||
i18nLibrary={i18nLibrary}
|
||||
onClick={(data, meta) => {
|
||||
if (meta || selectedItems.length > 0) {
|
||||
onClick(data);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ClassificationCard
|
||||
data={data}
|
||||
threshold={threshold}
|
||||
selected={false}
|
||||
i18nLibrary={i18nLibrary}
|
||||
onClick={(data, meta) => {
|
||||
if (meta || selectedItems.length > 0) {
|
||||
onClick(data);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children?.(data)}
|
||||
</ClassificationCard>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{children?.(data)}
|
||||
</ClassificationCard>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
</Content>
|
||||
|
||||
@ -51,7 +51,7 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { isDesktop, isMobile, isMobileOnly } from "react-device-detect";
|
||||
import { isDesktop } from "react-device-detect";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import {
|
||||
LuFolderCheck,
|
||||
@ -695,16 +695,13 @@ function TrainingGrid({
|
||||
<div
|
||||
ref={contentRef}
|
||||
className={cn(
|
||||
"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",
|
||||
"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",
|
||||
)}
|
||||
>
|
||||
{Object.entries(faceGroups).map(([key, group]) => {
|
||||
const event = events?.find((ev) => ev.id == key);
|
||||
return (
|
||||
<div key={key} className={cn(isMobile && "aspect-square size-full")}>
|
||||
<div key={key} className="aspect-square w-full">
|
||||
<FaceAttemptGroup
|
||||
config={config}
|
||||
group={group}
|
||||
@ -861,12 +858,12 @@ function FaceAttemptGroup({
|
||||
faceNames={faceNames}
|
||||
onTrainAttempt={(name) => onTrainAttempt(data, name)}
|
||||
>
|
||||
<AddFaceIcon className="size-7 cursor-pointer p-1 text-gray-200 hover:rounded-full hover:bg-primary-foreground" />
|
||||
<AddFaceIcon className="size-7 cursor-pointer p-1 text-gray-200 hover:rounded-full hover:bg-primary-foreground/40" />
|
||||
</FaceSelectionDialog>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<LuRefreshCw
|
||||
className="size-7 cursor-pointer p-1 text-gray-200 hover:rounded-full hover:bg-primary-foreground"
|
||||
className="size-7 cursor-pointer p-1 text-gray-200 hover:rounded-full hover:bg-primary-foreground/40"
|
||||
onClick={() => onReprocess(data)}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
@ -914,35 +911,35 @@ function FaceGrid({
|
||||
<div
|
||||
ref={contentRef}
|
||||
className={cn(
|
||||
"scrollbar-container gap-2 overflow-y-scroll p-1",
|
||||
isDesktop ? "flex flex-wrap" : "grid grid-cols-2 md:grid-cols-4",
|
||||
"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",
|
||||
)}
|
||||
>
|
||||
{sortedFaces.map((image: string) => (
|
||||
<ClassificationCard
|
||||
key={image}
|
||||
data={{
|
||||
name: pageToggle,
|
||||
filename: image,
|
||||
filepath: `clips/faces/${pageToggle}/${image}`,
|
||||
}}
|
||||
selected={selectedFaces.includes(image)}
|
||||
i18nLibrary="views/faceLibrary"
|
||||
onClick={(data, meta) => onClickFaces([data.filename], meta)}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<LuTrash2
|
||||
className="size-5 cursor-pointer text-gray-200 hover:text-danger"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete(pageToggle, [image]);
|
||||
}}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("button.deleteFaceAttempts")}</TooltipContent>
|
||||
</Tooltip>
|
||||
</ClassificationCard>
|
||||
<div key={image} className="aspect-square w-full">
|
||||
<ClassificationCard
|
||||
data={{
|
||||
name: pageToggle,
|
||||
filename: image,
|
||||
filepath: `clips/faces/${pageToggle}/${image}`,
|
||||
}}
|
||||
selected={selectedFaces.includes(image)}
|
||||
i18nLibrary="views/faceLibrary"
|
||||
onClick={(data, meta) => onClickFaces([data.filename], meta)}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<LuTrash2
|
||||
className="size-5 cursor-pointer text-gray-200 hover:text-danger"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete(pageToggle, [image]);
|
||||
}}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("button.deleteFaceAttempts")}</TooltipContent>
|
||||
</Tooltip>
|
||||
</ClassificationCard>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -44,7 +44,7 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { isDesktop, isMobile, isMobileOnly } from "react-device-detect";
|
||||
import { isDesktop } from "react-device-detect";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { LuPencil, LuTrash2 } from "react-icons/lu";
|
||||
import { toast } from "sonner";
|
||||
@ -632,36 +632,36 @@ function DatasetGrid({
|
||||
return (
|
||||
<div
|
||||
ref={contentRef}
|
||||
className="scrollbar-container flex flex-wrap gap-2 overflow-y-auto p-2"
|
||||
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"
|
||||
>
|
||||
{classData.map((image) => (
|
||||
<ClassificationCard
|
||||
key={image}
|
||||
imgClassName="size-auto"
|
||||
data={{
|
||||
filename: image,
|
||||
filepath: `clips/${modelName}/dataset/${categoryName}/${image}`,
|
||||
name: "",
|
||||
}}
|
||||
selected={selectedImages.includes(image)}
|
||||
i18nLibrary="views/classificationModel"
|
||||
onClick={(data, _) => onClickImages([data.filename], true)}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<LuTrash2
|
||||
className="size-5 cursor-pointer text-primary-variant hover:text-danger"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete([image]);
|
||||
}}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("button.deleteClassificationAttempts")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</ClassificationCard>
|
||||
<div key={image} className="aspect-square w-full">
|
||||
<ClassificationCard
|
||||
data={{
|
||||
filename: image,
|
||||
filepath: `clips/${modelName}/dataset/${categoryName}/${image}`,
|
||||
name: "",
|
||||
}}
|
||||
selected={selectedImages.includes(image)}
|
||||
i18nLibrary="views/classificationModel"
|
||||
onClick={(data, _) => onClickImages([data.filename], true)}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<LuTrash2
|
||||
className="size-5 cursor-pointer text-primary-variant hover:text-danger"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete([image]);
|
||||
}}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("button.deleteClassificationAttempts")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</ClassificationCard>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@ -791,30 +791,29 @@ function StateTrainGrid({
|
||||
<div
|
||||
ref={contentRef}
|
||||
className={cn(
|
||||
"scrollbar-container flex flex-wrap gap-3 overflow-y-auto p-2",
|
||||
isMobile && "justify-center",
|
||||
"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",
|
||||
)}
|
||||
>
|
||||
{trainData?.map((data) => (
|
||||
<ClassificationCard
|
||||
key={data.filename}
|
||||
imgClassName="size-auto"
|
||||
data={data}
|
||||
threshold={threshold}
|
||||
selected={selectedImages.includes(data.filename)}
|
||||
i18nLibrary="views/classificationModel"
|
||||
showArea={false}
|
||||
onClick={(data, meta) => onClickImages([data.filename], meta)}
|
||||
>
|
||||
<ClassificationSelectionDialog
|
||||
classes={classes}
|
||||
modelName={model.name}
|
||||
image={data.filename}
|
||||
onRefresh={onRefresh}
|
||||
<div key={data.filename} className="aspect-square w-full">
|
||||
<ClassificationCard
|
||||
data={data}
|
||||
threshold={threshold}
|
||||
selected={selectedImages.includes(data.filename)}
|
||||
i18nLibrary="views/classificationModel"
|
||||
showArea={false}
|
||||
onClick={(data, meta) => onClickImages([data.filename], meta)}
|
||||
>
|
||||
<TbCategoryPlus className="size-7 cursor-pointer p-1 text-gray-200 hover:rounded-full hover:bg-primary-foreground" />
|
||||
</ClassificationSelectionDialog>
|
||||
</ClassificationCard>
|
||||
<ClassificationSelectionDialog
|
||||
classes={classes}
|
||||
modelName={model.name}
|
||||
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>
|
||||
);
|
||||
@ -928,21 +927,14 @@ function ObjectTrainGrid({
|
||||
<div
|
||||
ref={contentRef}
|
||||
className={cn(
|
||||
"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",
|
||||
"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",
|
||||
)}
|
||||
>
|
||||
{Object.entries(groups).map(([key, group]) => {
|
||||
const event = events?.find((ev) => ev.id == key);
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className={cn(isMobile && "aspect-square size-full")}
|
||||
>
|
||||
<div key={key} className="aspect-square w-full">
|
||||
<GroupedClassificationCard
|
||||
key={key}
|
||||
group={group}
|
||||
event={event}
|
||||
threshold={threshold}
|
||||
@ -965,7 +957,7 @@ function ObjectTrainGrid({
|
||||
image={data.filename}
|
||||
onRefresh={onRefresh}
|
||||
>
|
||||
<TbCategoryPlus className="size-7 cursor-pointer p-1 text-gray-200 hover:rounded-full hover:bg-primary-foreground" />
|
||||
<TbCategoryPlus className="size-7 cursor-pointer p-1 text-gray-200 hover:rounded-full hover:bg-primary-foreground/40" />
|
||||
</ClassificationSelectionDialog>
|
||||
</>
|
||||
)}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user