mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-01-22 20:18:30 +03:00
Miscellaneous Fixes (0.17 beta) (#21575)
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
* icon improvements add type to getIconForLabel provide default icon for audio events * Add preferred language to review docs * prevent react Suspense crash during auth redirect add redirect-check guards to stop rendering lazy routes while navigation is pending (fixes some users seeing React error #426 when auth expires) * Uppsercase model name --------- Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
This commit is contained in:
parent
f3543cfee2
commit
c08fa15724
@ -112,6 +112,17 @@ review:
|
|||||||
- animals in the garden
|
- animals in the garden
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Preferred Language
|
||||||
|
|
||||||
|
By default, review summaries are generated in English. You can configure Frigate to generate summaries in your preferred language by setting the `preferred_language` option:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
review:
|
||||||
|
genai:
|
||||||
|
enabled: true
|
||||||
|
preferred_language: Spanish
|
||||||
|
```
|
||||||
|
|
||||||
## Review Reports
|
## Review Reports
|
||||||
|
|
||||||
Along with individual review item summaries, Generative AI provides the ability to request a report of a given time period. For example, you can get a daily report while on a vacation of any suspicious activity or other concerns that may require review.
|
Along with individual review item summaries, Generative AI provides the ability to request a report of a given time period. For example, you can get a daily report while on a vacation of any suspicious activity or other concerns that may require review.
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import { AuthProvider } from "@/context/auth-context";
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { FrigateConfig } from "./types/frigateConfig";
|
import { FrigateConfig } from "./types/frigateConfig";
|
||||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||||
|
import { isRedirectingToLogin } from "@/api/auth-redirect";
|
||||||
|
|
||||||
const Live = lazy(() => import("@/pages/Live"));
|
const Live = lazy(() => import("@/pages/Live"));
|
||||||
const Events = lazy(() => import("@/pages/Events"));
|
const Events = lazy(() => import("@/pages/Events"));
|
||||||
@ -58,6 +59,16 @@ function DefaultAppView() {
|
|||||||
? Object.keys(config.auth.roles)
|
? Object.keys(config.auth.roles)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
// Show loading indicator during redirect to prevent React from attempting to render
|
||||||
|
// lazy components, which would cause error #426 (suspension during synchronous navigation)
|
||||||
|
if (isRedirectingToLogin()) {
|
||||||
|
return (
|
||||||
|
<div className="size-full overflow-hidden">
|
||||||
|
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="size-full overflow-hidden">
|
<div className="size-full overflow-hidden">
|
||||||
{isDesktop && <Sidebar />}
|
{isDesktop && <Sidebar />}
|
||||||
|
|||||||
@ -28,6 +28,14 @@ export default function ProtectedRoute({
|
|||||||
}
|
}
|
||||||
}, [auth.isLoading, auth.isAuthenticated, auth.user]);
|
}, [auth.isLoading, auth.isAuthenticated, auth.user]);
|
||||||
|
|
||||||
|
// Show loading indicator during redirect to prevent React from attempting to render
|
||||||
|
// lazy components, which would cause error #426 (suspension during synchronous navigation)
|
||||||
|
if (isRedirectingToLogin()) {
|
||||||
|
return (
|
||||||
|
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (auth.isLoading) {
|
if (auth.isLoading) {
|
||||||
return (
|
return (
|
||||||
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
||||||
|
|||||||
@ -181,7 +181,7 @@ export default function ReviewCard({
|
|||||||
key={`${object}-${idx}`}
|
key={`${object}-${idx}`}
|
||||||
className="rounded-full bg-muted-foreground p-1"
|
className="rounded-full bg-muted-foreground p-1"
|
||||||
>
|
>
|
||||||
{getIconForLabel(object, "size-3 text-white")}
|
{getIconForLabel(object, "object", "size-3 text-white")}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{event.data.audio.map((audio, idx) => (
|
{event.data.audio.map((audio, idx) => (
|
||||||
@ -189,7 +189,7 @@ export default function ReviewCard({
|
|||||||
key={`${audio}-${idx}`}
|
key={`${audio}-${idx}`}
|
||||||
className="rounded-full bg-muted-foreground p-1"
|
className="rounded-full bg-muted-foreground p-1"
|
||||||
>
|
>
|
||||||
{getIconForLabel(audio, "size-3 text-white")}
|
{getIconForLabel(audio, "audio", "size-3 text-white")}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -133,7 +133,11 @@ export default function SearchThumbnail({
|
|||||||
className={`z-0 flex items-center justify-between gap-1 space-x-1 bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 text-xs capitalize`}
|
className={`z-0 flex items-center justify-between gap-1 space-x-1 bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 text-xs capitalize`}
|
||||||
onClick={() => onClick(searchResult, false, true)}
|
onClick={() => onClick(searchResult, false, true)}
|
||||||
>
|
>
|
||||||
{getIconForLabel(objectLabel, "size-3 text-white")}
|
{getIconForLabel(
|
||||||
|
objectLabel,
|
||||||
|
searchResult.data.type,
|
||||||
|
"size-3 text-white",
|
||||||
|
)}
|
||||||
{Math.floor(
|
{Math.floor(
|
||||||
(searchResult.data.score ??
|
(searchResult.data.score ??
|
||||||
searchResult.data.top_score ??
|
searchResult.data.top_score ??
|
||||||
|
|||||||
@ -1296,7 +1296,11 @@ function ObjectDetailsTab({
|
|||||||
{t("details.label")}
|
{t("details.label")}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center gap-2 text-sm smart-capitalize">
|
<div className="flex flex-row items-center gap-2 text-sm smart-capitalize">
|
||||||
{getIconForLabel(search.label, "size-4 text-primary")}
|
{getIconForLabel(
|
||||||
|
search.label,
|
||||||
|
search.data.type,
|
||||||
|
"size-4 text-primary",
|
||||||
|
)}
|
||||||
{getTranslatedLabel(search.label, search.data.type)}
|
{getTranslatedLabel(search.label, search.data.type)}
|
||||||
{search.sub_label && ` (${search.sub_label})`}
|
{search.sub_label && ` (${search.sub_label})`}
|
||||||
{isAdmin && search.end_time && (
|
{isAdmin && search.end_time && (
|
||||||
|
|||||||
@ -665,6 +665,7 @@ export function TrackingDetails({
|
|||||||
>
|
>
|
||||||
{getIconForLabel(
|
{getIconForLabel(
|
||||||
event.sub_label ? event.label + "-verified" : event.label,
|
event.sub_label ? event.label + "-verified" : event.label,
|
||||||
|
event.data.type,
|
||||||
"size-4 text-white",
|
"size-4 text-white",
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -359,7 +359,11 @@ export default function LivePlayer({
|
|||||||
]),
|
]),
|
||||||
]
|
]
|
||||||
.map((label) => {
|
.map((label) => {
|
||||||
return getIconForLabel(label, "size-3 text-white");
|
return getIconForLabel(
|
||||||
|
label,
|
||||||
|
"object",
|
||||||
|
"size-3 text-white",
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.sort()}
|
.sort()}
|
||||||
</Chip>
|
</Chip>
|
||||||
|
|||||||
@ -262,10 +262,18 @@ export default function PreviewThumbnailPlayer({
|
|||||||
onClick={() => onClick(review, false, true)}
|
onClick={() => onClick(review, false, true)}
|
||||||
>
|
>
|
||||||
{review.data.objects.sort().map((object) => {
|
{review.data.objects.sort().map((object) => {
|
||||||
return getIconForLabel(object, "size-3 text-white");
|
return getIconForLabel(
|
||||||
|
object,
|
||||||
|
"object",
|
||||||
|
"size-3 text-white",
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
{review.data.audio.map((audio) => {
|
{review.data.audio.map((audio) => {
|
||||||
return getIconForLabel(audio, "size-3 text-white");
|
return getIconForLabel(
|
||||||
|
audio,
|
||||||
|
"audio",
|
||||||
|
"size-3 text-white",
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</Chip>
|
</Chip>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import { FrigateConfig } from "@/types/frigateConfig";
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import ActivityIndicator from "../indicators/activity-indicator";
|
import ActivityIndicator from "../indicators/activity-indicator";
|
||||||
import { Event } from "@/types/event";
|
import { Event } from "@/types/event";
|
||||||
|
import { EventType } from "@/types/search";
|
||||||
import { getIconForLabel } from "@/utils/iconUtil";
|
import { getIconForLabel } from "@/utils/iconUtil";
|
||||||
import { REVIEW_PADDING, ReviewSegment } from "@/types/review";
|
import { REVIEW_PADDING, ReviewSegment } from "@/types/review";
|
||||||
import { LuChevronDown, LuCircle, LuChevronRight } from "react-icons/lu";
|
import { LuChevronDown, LuCircle, LuChevronRight } from "react-icons/lu";
|
||||||
@ -346,22 +347,29 @@ function ReviewGroup({
|
|||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const rawIconLabels: string[] = [
|
const rawIconLabels: Array<{ label: string; type: EventType }> = [
|
||||||
...(fetchedEvents
|
...(fetchedEvents
|
||||||
? fetchedEvents.map((e) =>
|
? fetchedEvents.map((e) => ({
|
||||||
e.sub_label ? e.label + "-verified" : e.label,
|
label: e.sub_label ? e.label + "-verified" : e.label,
|
||||||
)
|
type: e.data.type,
|
||||||
: (review.data?.objects ?? [])),
|
}))
|
||||||
...(review.data?.audio ?? []),
|
: (review.data?.objects ?? []).map((obj) => ({
|
||||||
|
label: obj,
|
||||||
|
type: "object" as EventType,
|
||||||
|
}))),
|
||||||
|
...(review.data?.audio ?? []).map((audio) => ({
|
||||||
|
label: audio,
|
||||||
|
type: "audio" as EventType,
|
||||||
|
})),
|
||||||
];
|
];
|
||||||
|
|
||||||
// limit to 5 icons
|
// limit to 5 icons
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const iconLabels: string[] = [];
|
const iconLabels: Array<{ label: string; type: EventType }> = [];
|
||||||
for (const lbl of rawIconLabels) {
|
for (const item of rawIconLabels) {
|
||||||
if (!seen.has(lbl)) {
|
if (!seen.has(item.label)) {
|
||||||
seen.add(lbl);
|
seen.add(item.label);
|
||||||
iconLabels.push(lbl);
|
iconLabels.push(item);
|
||||||
if (iconLabels.length >= 5) break;
|
if (iconLabels.length >= 5) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -418,12 +426,12 @@ function ReviewGroup({
|
|||||||
<div className="flex flex-row gap-3">
|
<div className="flex flex-row gap-3">
|
||||||
<div className="text-sm font-medium">{displayTime}</div>
|
<div className="text-sm font-medium">{displayTime}</div>
|
||||||
<div className="relative flex items-center gap-2 text-white">
|
<div className="relative flex items-center gap-2 text-white">
|
||||||
{iconLabels.slice(0, 5).map((lbl, idx) => (
|
{iconLabels.slice(0, 5).map(({ label: lbl, type }, idx) => (
|
||||||
<div
|
<div
|
||||||
key={`${lbl}-${idx}`}
|
key={`${lbl}-${idx}`}
|
||||||
className="rounded-full bg-muted-foreground p-1"
|
className="rounded-full bg-muted-foreground p-1"
|
||||||
>
|
>
|
||||||
{getIconForLabel(lbl, "size-3 text-white")}
|
{getIconForLabel(lbl, type, "size-3 text-white")}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -516,7 +524,11 @@ function ReviewGroup({
|
|||||||
>
|
>
|
||||||
<div className="ml-1.5 flex items-center gap-2 text-sm font-medium">
|
<div className="ml-1.5 flex items-center gap-2 text-sm font-medium">
|
||||||
<div className="rounded-full bg-muted-foreground p-1">
|
<div className="rounded-full bg-muted-foreground p-1">
|
||||||
{getIconForLabel(audioLabel, "size-3 text-white")}
|
{getIconForLabel(
|
||||||
|
audioLabel,
|
||||||
|
"audio",
|
||||||
|
"size-3 text-white",
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span>{getTranslatedLabel(audioLabel, "audio")}</span>
|
<span>{getTranslatedLabel(audioLabel, "audio")}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -618,6 +630,7 @@ function EventList({
|
|||||||
>
|
>
|
||||||
{getIconForLabel(
|
{getIconForLabel(
|
||||||
event.sub_label ? event.label + "-verified" : event.label,
|
event.sub_label ? event.label + "-verified" : event.label,
|
||||||
|
event.data.type,
|
||||||
"size-3 text-white",
|
"size-3 text-white",
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { IconName } from "@/components/icons/IconPicker";
|
import { IconName } from "@/components/icons/IconPicker";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
|
import { EventType } from "@/types/search";
|
||||||
import { BsPersonWalking } from "react-icons/bs";
|
import { BsPersonWalking } from "react-icons/bs";
|
||||||
import {
|
import {
|
||||||
FaAmazon,
|
FaAmazon,
|
||||||
@ -32,6 +33,7 @@ import {
|
|||||||
GiRabbit,
|
GiRabbit,
|
||||||
GiRaccoonHead,
|
GiRaccoonHead,
|
||||||
GiSailboat,
|
GiSailboat,
|
||||||
|
GiSoundWaves,
|
||||||
GiSquirrel,
|
GiSquirrel,
|
||||||
} from "react-icons/gi";
|
} from "react-icons/gi";
|
||||||
import { LuBox, LuLassoSelect, LuScanBarcode } from "react-icons/lu";
|
import { LuBox, LuLassoSelect, LuScanBarcode } from "react-icons/lu";
|
||||||
@ -56,11 +58,15 @@ export function isValidIconName(value: string): value is IconName {
|
|||||||
return Object.keys(LuIcons).includes(value as IconName);
|
return Object.keys(LuIcons).includes(value as IconName);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getIconForLabel(label: string, className?: string) {
|
export function getIconForLabel(
|
||||||
|
label: string,
|
||||||
|
type: EventType = "object",
|
||||||
|
className?: string,
|
||||||
|
) {
|
||||||
if (label.endsWith("-verified")) {
|
if (label.endsWith("-verified")) {
|
||||||
return getVerifiedIcon(label, className);
|
return getVerifiedIcon(label, className, type);
|
||||||
} else if (label.endsWith("-plate")) {
|
} else if (label.endsWith("-plate")) {
|
||||||
return getRecognizedPlateIcon(label, className);
|
return getRecognizedPlateIcon(label, className, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (label) {
|
switch (label) {
|
||||||
@ -152,27 +158,38 @@ export function getIconForLabel(label: string, className?: string) {
|
|||||||
case "usps":
|
case "usps":
|
||||||
return <FaUsps key={label} className={className} />;
|
return <FaUsps key={label} className={className} />;
|
||||||
default:
|
default:
|
||||||
|
if (type === "audio") {
|
||||||
|
return <GiSoundWaves key={label} className={className} />;
|
||||||
|
}
|
||||||
return <LuLassoSelect key={label} className={className} />;
|
return <LuLassoSelect key={label} className={className} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVerifiedIcon(label: string, className?: string) {
|
function getVerifiedIcon(
|
||||||
|
label: string,
|
||||||
|
className?: string,
|
||||||
|
type: EventType = "object",
|
||||||
|
) {
|
||||||
const simpleLabel = label.substring(0, label.lastIndexOf("-"));
|
const simpleLabel = label.substring(0, label.lastIndexOf("-"));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={label} className="flex items-center">
|
<div key={label} className="flex items-center">
|
||||||
{getIconForLabel(simpleLabel, className)}
|
{getIconForLabel(simpleLabel, type, className)}
|
||||||
<FaCheckCircle className="absolute size-2 translate-x-[80%] translate-y-3/4" />
|
<FaCheckCircle className="absolute size-2 translate-x-[80%] translate-y-3/4" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRecognizedPlateIcon(label: string, className?: string) {
|
function getRecognizedPlateIcon(
|
||||||
|
label: string,
|
||||||
|
className?: string,
|
||||||
|
type: EventType = "object",
|
||||||
|
) {
|
||||||
const simpleLabel = label.substring(0, label.lastIndexOf("-"));
|
const simpleLabel = label.substring(0, label.lastIndexOf("-"));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={label} className="flex items-center">
|
<div key={label} className="flex items-center">
|
||||||
{getIconForLabel(simpleLabel, className)}
|
{getIconForLabel(simpleLabel, type, className)}
|
||||||
<LuScanBarcode className="absolute size-2.5 translate-x-[50%] translate-y-3/4" />
|
<LuScanBarcode className="absolute size-2.5 translate-x-[50%] translate-y-3/4" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -88,7 +88,7 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
|
|||||||
// title
|
// title
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = `${model.name} - ${t("documentTitle")}`;
|
document.title = `${model.name.toUpperCase()} - ${t("documentTitle")}`;
|
||||||
}, [model.name, t]);
|
}, [model.name, t]);
|
||||||
|
|
||||||
// model state
|
// model state
|
||||||
|
|||||||
@ -406,7 +406,7 @@ function ObjectList({ cameraConfig, objects }: ObjectListProps) {
|
|||||||
: getColorForObjectName(obj.label),
|
: getColorForObjectName(obj.label),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{getIconForLabel(obj.label, "size-5 text-white")}
|
{getIconForLabel(obj.label, "object", "size-5 text-white")}
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-3 text-lg">
|
<div className="ml-3 text-lg">
|
||||||
{getTranslatedLabel(obj.label)}
|
{getTranslatedLabel(obj.label)}
|
||||||
@ -494,7 +494,7 @@ function AudioList({ cameraConfig, audioDetections }: AudioListProps) {
|
|||||||
<div className="flex flex-row items-center gap-3 pb-1">
|
<div className="flex flex-row items-center gap-3 pb-1">
|
||||||
<div className="flex flex-1 flex-row items-center justify-start p-3 pl-1">
|
<div className="flex flex-1 flex-row items-center justify-start p-3 pl-1">
|
||||||
<div className="rounded-lg bg-selected p-2">
|
<div className="rounded-lg bg-selected p-2">
|
||||||
{getIconForLabel(key, "size-5 text-white")}
|
{getIconForLabel(key, "audio", "size-5 text-white")}
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-3 text-lg">{getTranslatedLabel(key)}</div>
|
<div className="ml-3 text-lg">{getTranslatedLabel(key)}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user