mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-19 11:36:43 +03:00
Compare commits
3 Commits
256817d5c2
...
3b2d136665
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b2d136665 | ||
|
|
e7394d0dc1 | ||
|
|
2e288109f4 |
@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"documentTitle": "Classification Models",
|
"documentTitle": "Classification Models",
|
||||||
|
"details": {
|
||||||
|
"scoreInfo": "Score represents the average classification confidence across all detections of this object."
|
||||||
|
},
|
||||||
"button": {
|
"button": {
|
||||||
"deleteClassificationAttempts": "Delete Classification Images",
|
"deleteClassificationAttempts": "Delete Classification Images",
|
||||||
"renameCategory": "Rename Class",
|
"renameCategory": "Rename Class",
|
||||||
|
|||||||
@ -6,7 +6,8 @@
|
|||||||
},
|
},
|
||||||
"details": {
|
"details": {
|
||||||
"timestamp": "Timestamp",
|
"timestamp": "Timestamp",
|
||||||
"unknown": "Unknown"
|
"unknown": "Unknown",
|
||||||
|
"scoreInfo": "Score is a weighted average of all face scores, weighted by the size of the face in each image."
|
||||||
},
|
},
|
||||||
"documentTitle": "Face Library - Frigate",
|
"documentTitle": "Face Library - Frigate",
|
||||||
"uploadFaceImage": {
|
"uploadFaceImage": {
|
||||||
|
|||||||
@ -11,7 +11,8 @@ import { isDesktop, isMobile } from "react-device-detect";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import TimeAgo from "../dynamic/TimeAgo";
|
import TimeAgo from "../dynamic/TimeAgo";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||||
import { LuSearch } from "react-icons/lu";
|
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
||||||
|
import { LuSearch, LuInfo } from "react-icons/lu";
|
||||||
import { TooltipPortal } from "@radix-ui/react-tooltip";
|
import { TooltipPortal } from "@radix-ui/react-tooltip";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { HiSquare2Stack } from "react-icons/hi2";
|
import { HiSquare2Stack } from "react-icons/hi2";
|
||||||
@ -321,14 +322,31 @@ export function GroupedClassificationCard({
|
|||||||
? event.sub_label
|
? event.sub_label
|
||||||
: t(noClassificationLabel)}
|
: t(noClassificationLabel)}
|
||||||
{event?.sub_label && event.sub_label !== "none" && (
|
{event?.sub_label && event.sub_label !== "none" && (
|
||||||
<div
|
<div className="flex items-center gap-1">
|
||||||
className={cn(
|
<div
|
||||||
"",
|
className={cn(
|
||||||
bestScoreStatus == "match" && "text-success",
|
"",
|
||||||
bestScoreStatus == "potential" && "text-orange-400",
|
bestScoreStatus == "match" && "text-success",
|
||||||
bestScoreStatus == "unknown" && "text-danger",
|
bestScoreStatus == "potential" && "text-orange-400",
|
||||||
)}
|
bestScoreStatus == "unknown" && "text-danger",
|
||||||
>{`${Math.round((event.data.sub_label_score || 0) * 100)}%`}</div>
|
)}
|
||||||
|
>{`${Math.round((event.data.sub_label_score || 0) * 100)}%`}</div>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<button
|
||||||
|
className="focus:outline-none"
|
||||||
|
aria-label={t("details.scoreInfo", {
|
||||||
|
ns: i18nLibrary,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<LuInfo className="size-3" />
|
||||||
|
</button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-80 text-sm">
|
||||||
|
{t("details.scoreInfo", { ns: i18nLibrary })}
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</ContentTitle>
|
</ContentTitle>
|
||||||
<ContentDescription className={cn("", isMobile && "px-2")}>
|
<ContentDescription className={cn("", isMobile && "px-2")}>
|
||||||
|
|||||||
@ -37,6 +37,7 @@ import { capitalizeFirstLetter } from "@/utils/stringUtil";
|
|||||||
import { Button, buttonVariants } from "../ui/button";
|
import { Button, buttonVariants } from "../ui/button";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { LuCircle } from "react-icons/lu";
|
||||||
|
|
||||||
type ReviewCardProps = {
|
type ReviewCardProps = {
|
||||||
event: ReviewSegment;
|
event: ReviewSegment;
|
||||||
@ -142,7 +143,7 @@ export default function ReviewCard({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"size-full rounded-lg",
|
"size-full rounded-lg",
|
||||||
activeReviewItem?.id == event.id &&
|
activeReviewItem?.id == event.id &&
|
||||||
"outline outline-[3px] outline-offset-1 outline-selected",
|
"outline outline-[3px] -outline-offset-[2.8px] outline-selected duration-200",
|
||||||
imgLoaded ? "visible" : "invisible",
|
imgLoaded ? "visible" : "invisible",
|
||||||
)}
|
)}
|
||||||
src={`${baseUrl}${event.thumb_path.replace("/media/frigate/", "")}`}
|
src={`${baseUrl}${event.thumb_path.replace("/media/frigate/", "")}`}
|
||||||
@ -165,6 +166,14 @@ export default function ReviewCard({
|
|||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="flex items-center justify-evenly gap-1">
|
<div className="flex items-center justify-evenly gap-1">
|
||||||
<>
|
<>
|
||||||
|
<LuCircle
|
||||||
|
className={cn(
|
||||||
|
"size-2",
|
||||||
|
event.severity == "alert"
|
||||||
|
? "fill-severity_alert text-severity_alert"
|
||||||
|
: "fill-severity_detection text-severity_detection",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
{event.data.objects.map((object) => {
|
{event.data.objects.map((object) => {
|
||||||
return getIconForLabel(
|
return getIconForLabel(
|
||||||
object,
|
object,
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
import { generateFixedHash, isValidId } from "@/utils/stringUtil";
|
import { generateFixedHash, isValidId } from "@/utils/stringUtil";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@ -41,8 +41,9 @@ export default function NameAndIdFields<T extends FieldValues = FieldValues>({
|
|||||||
placeholderId,
|
placeholderId,
|
||||||
}: NameAndIdFieldsProps<T>) {
|
}: NameAndIdFieldsProps<T>) {
|
||||||
const { t } = useTranslation(["common"]);
|
const { t } = useTranslation(["common"]);
|
||||||
const { watch, setValue, trigger } = useFormContext<T>();
|
const { watch, setValue, trigger, formState } = useFormContext<T>();
|
||||||
const [isIdVisible, setIsIdVisible] = useState(false);
|
const [isIdVisible, setIsIdVisible] = useState(false);
|
||||||
|
const hasUserTypedRef = useRef(false);
|
||||||
|
|
||||||
const defaultProcessId = (name: string) => {
|
const defaultProcessId = (name: string) => {
|
||||||
const normalized = name.replace(/\s+/g, "_").toLowerCase();
|
const normalized = name.replace(/\s+/g, "_").toLowerCase();
|
||||||
@ -58,6 +59,7 @@ export default function NameAndIdFields<T extends FieldValues = FieldValues>({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const subscription = watch((value, { name }) => {
|
const subscription = watch((value, { name }) => {
|
||||||
if (name === nameField) {
|
if (name === nameField) {
|
||||||
|
hasUserTypedRef.current = true;
|
||||||
const processedId = effectiveProcessId(value[nameField] || "");
|
const processedId = effectiveProcessId(value[nameField] || "");
|
||||||
setValue(idField, processedId as PathValue<T, Path<T>>);
|
setValue(idField, processedId as PathValue<T, Path<T>>);
|
||||||
trigger(idField);
|
trigger(idField);
|
||||||
@ -66,6 +68,14 @@ export default function NameAndIdFields<T extends FieldValues = FieldValues>({
|
|||||||
return () => subscription.unsubscribe();
|
return () => subscription.unsubscribe();
|
||||||
}, [watch, setValue, trigger, nameField, idField, effectiveProcessId]);
|
}, [watch, setValue, trigger, nameField, idField, effectiveProcessId]);
|
||||||
|
|
||||||
|
// Auto-expand if there's an error on the ID field after user has typed
|
||||||
|
useEffect(() => {
|
||||||
|
const idError = formState.errors[idField];
|
||||||
|
if (idError && hasUserTypedRef.current && !isIdVisible) {
|
||||||
|
setIsIdVisible(true);
|
||||||
|
}
|
||||||
|
}, [formState.errors, idField, isIdVisible]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormField
|
<FormField
|
||||||
|
|||||||
@ -289,6 +289,7 @@ export default function VideoControls({
|
|||||||
}}
|
}}
|
||||||
onUploadFrame={onUploadFrame}
|
onUploadFrame={onUploadFrame}
|
||||||
containerRef={containerRef}
|
containerRef={containerRef}
|
||||||
|
fullscreen={fullscreen}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{features.fullscreen && toggleFullscreen && (
|
{features.fullscreen && toggleFullscreen && (
|
||||||
@ -306,6 +307,7 @@ type FrigatePlusUploadButtonProps = {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onUploadFrame: () => void;
|
onUploadFrame: () => void;
|
||||||
containerRef?: React.MutableRefObject<HTMLDivElement | null>;
|
containerRef?: React.MutableRefObject<HTMLDivElement | null>;
|
||||||
|
fullscreen?: boolean;
|
||||||
};
|
};
|
||||||
function FrigatePlusUploadButton({
|
function FrigatePlusUploadButton({
|
||||||
video,
|
video,
|
||||||
@ -313,6 +315,7 @@ function FrigatePlusUploadButton({
|
|||||||
onClose,
|
onClose,
|
||||||
onUploadFrame,
|
onUploadFrame,
|
||||||
containerRef,
|
containerRef,
|
||||||
|
fullscreen,
|
||||||
}: FrigatePlusUploadButtonProps) {
|
}: FrigatePlusUploadButtonProps) {
|
||||||
const { t } = useTranslation(["components/player"]);
|
const { t } = useTranslation(["components/player"]);
|
||||||
|
|
||||||
@ -349,7 +352,11 @@ function FrigatePlusUploadButton({
|
|||||||
/>
|
/>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent
|
<AlertDialogContent
|
||||||
portalProps={{ container: containerRef?.current }}
|
portalProps={
|
||||||
|
fullscreen && containerRef?.current
|
||||||
|
? { container: containerRef.current }
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
className="md:max-w-2xl lg:max-w-3xl xl:max-w-4xl"
|
className="md:max-w-2xl lg:max-w-3xl xl:max-w-4xl"
|
||||||
>
|
>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
|
|||||||
@ -367,7 +367,11 @@ function ReviewGroup({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-review-id={id}
|
data-review-id={id}
|
||||||
className="cursor-pointer rounded-lg bg-secondary py-3"
|
className={`mx-1 cursor-pointer rounded-lg bg-secondary px-0 py-3 outline outline-[2px] -outline-offset-[1.8px] ${
|
||||||
|
isActive
|
||||||
|
? "shadow-selected outline-selected"
|
||||||
|
: "outline-transparent duration-500"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -382,10 +386,10 @@ function ReviewGroup({
|
|||||||
<div className="ml-4 mr-2 mt-1.5 flex flex-row items-start">
|
<div className="ml-4 mr-2 mt-1.5 flex flex-row items-start">
|
||||||
<LuCircle
|
<LuCircle
|
||||||
className={cn(
|
className={cn(
|
||||||
"size-3",
|
"size-3 duration-500",
|
||||||
isActive
|
review.severity == "alert"
|
||||||
? "fill-selected text-selected"
|
? "fill-severity_alert text-severity_alert"
|
||||||
: "fill-muted duration-500 dark:fill-secondary-highlight dark:text-secondary-highlight",
|
: "fill-severity_detection text-severity_detection",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -43,5 +43,5 @@ export function generateFixedHash(name: string, prefix: string = "id"): string {
|
|||||||
* @returns True if the name is valid, false otherwise
|
* @returns True if the name is valid, false otherwise
|
||||||
*/
|
*/
|
||||||
export function isValidId(name: string): boolean {
|
export function isValidId(name: string): boolean {
|
||||||
return /^[a-zA-Z0-9_-]+$/.test(name);
|
return /^[a-zA-Z0-9_-]+$/.test(name) && !/^\d+$/.test(name);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user