Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/docs/configuration/genai/review_summaries.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,17 @@ review:
- 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

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.
Expand Down
11 changes: 11 additions & 0 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { AuthProvider } from "@/context/auth-context";
import useSWR from "swr";
import { FrigateConfig } from "./types/frigateConfig";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import { isRedirectingToLogin } from "@/api/auth-redirect";

const Live = lazy(() => import("@/pages/Live"));
const Events = lazy(() => import("@/pages/Events"));
Expand Down Expand Up @@ -58,6 +59,16 @@ function DefaultAppView() {
? Object.keys(config.auth.roles)
: 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 (
<div className="size-full overflow-hidden">
{isDesktop && <Sidebar />}
Expand Down
8 changes: 8 additions & 0 deletions web/src/components/auth/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ export default function ProtectedRoute({
}
}, [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) {
return (
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
Expand Down
4 changes: 2 additions & 2 deletions web/src/components/card/ReviewCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,15 +181,15 @@ export default function ReviewCard({
key={`${object}-${idx}`}
className="rounded-full bg-muted-foreground p-1"
>
{getIconForLabel(object, "size-3 text-white")}
{getIconForLabel(object, "object", "size-3 text-white")}
</div>
))}
{event.data.audio.map((audio, idx) => (
<div
key={`${audio}-${idx}`}
className="rounded-full bg-muted-foreground p-1"
>
{getIconForLabel(audio, "size-3 text-white")}
{getIconForLabel(audio, "audio", "size-3 text-white")}
</div>
))}
</div>
Expand Down
6 changes: 5 additions & 1 deletion web/src/components/card/SearchThumbnail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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`}
onClick={() => onClick(searchResult, false, true)}
>
{getIconForLabel(objectLabel, "size-3 text-white")}
{getIconForLabel(
objectLabel,
searchResult.data.type,
"size-3 text-white",
)}
{Math.floor(
(searchResult.data.score ??
searchResult.data.top_score ??
Expand Down
6 changes: 5 additions & 1 deletion web/src/components/overlay/detail/SearchDetailDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1296,7 +1296,11 @@ function ObjectDetailsTab({
{t("details.label")}
</div>
<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)}
{search.sub_label && ` (${search.sub_label})`}
{isAdmin && search.end_time && (
Expand Down
1 change: 1 addition & 0 deletions web/src/components/overlay/detail/TrackingDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,7 @@ export function TrackingDetails({
>
{getIconForLabel(
event.sub_label ? event.label + "-verified" : event.label,
event.data.type,
"size-4 text-white",
)}
</div>
Expand Down
6 changes: 5 additions & 1 deletion web/src/components/player/LivePlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,11 @@ export default function LivePlayer({
]),
]
.map((label) => {
return getIconForLabel(label, "size-3 text-white");
return getIconForLabel(
label,
"object",
"size-3 text-white",
);
})
.sort()}
</Chip>
Expand Down
12 changes: 10 additions & 2 deletions web/src/components/player/PreviewThumbnailPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -262,10 +262,18 @@ export default function PreviewThumbnailPlayer({
onClick={() => onClick(review, false, true)}
>
{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) => {
return getIconForLabel(audio, "size-3 text-white");
return getIconForLabel(
audio,
"audio",
"size-3 text-white",
);
})}
</Chip>
</>
Expand Down
41 changes: 27 additions & 14 deletions web/src/components/timeline/DetailStream.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { FrigateConfig } from "@/types/frigateConfig";
import useSWR from "swr";
import ActivityIndicator from "../indicators/activity-indicator";
import { Event } from "@/types/event";
import { EventType } from "@/types/search";
import { getIconForLabel } from "@/utils/iconUtil";
import { REVIEW_PADDING, ReviewSegment } from "@/types/review";
import { LuChevronDown, LuCircle, LuChevronRight } from "react-icons/lu";
Expand Down Expand Up @@ -346,22 +347,29 @@ function ReviewGroup({
: null,
);

const rawIconLabels: string[] = [
const rawIconLabels: Array<{ label: string; type: EventType }> = [
...(fetchedEvents
? fetchedEvents.map((e) =>
e.sub_label ? e.label + "-verified" : e.label,
)
: (review.data?.objects ?? [])),
...(review.data?.audio ?? []),
? fetchedEvents.map((e) => ({
label: e.sub_label ? e.label + "-verified" : e.label,
type: e.data.type,
}))
: (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
const seen = new Set<string>();
const iconLabels: string[] = [];
for (const lbl of rawIconLabels) {
if (!seen.has(lbl)) {
seen.add(lbl);
iconLabels.push(lbl);
const iconLabels: Array<{ label: string; type: EventType }> = [];
for (const item of rawIconLabels) {
if (!seen.has(item.label)) {
seen.add(item.label);
iconLabels.push(item);
if (iconLabels.length >= 5) break;
}
}
Expand Down Expand Up @@ -418,12 +426,12 @@ function ReviewGroup({
<div className="flex flex-row gap-3">
<div className="text-sm font-medium">{displayTime}</div>
<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
key={`${lbl}-${idx}`}
className="rounded-full bg-muted-foreground p-1"
>
{getIconForLabel(lbl, "size-3 text-white")}
{getIconForLabel(lbl, type, "size-3 text-white")}
</div>
))}
</div>
Expand Down Expand Up @@ -516,7 +524,11 @@ function ReviewGroup({
>
<div className="ml-1.5 flex items-center gap-2 text-sm font-medium">
<div className="rounded-full bg-muted-foreground p-1">
{getIconForLabel(audioLabel, "size-3 text-white")}
{getIconForLabel(
audioLabel,
"audio",
"size-3 text-white",
)}
</div>
<span>{getTranslatedLabel(audioLabel, "audio")}</span>
</div>
Expand Down Expand Up @@ -618,6 +630,7 @@ function EventList({
>
{getIconForLabel(
event.sub_label ? event.label + "-verified" : event.label,
event.data.type,
"size-3 text-white",
)}
</div>
Expand Down
31 changes: 24 additions & 7 deletions web/src/utils/iconUtil.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { IconName } from "@/components/icons/IconPicker";
import { FrigateConfig } from "@/types/frigateConfig";
import { EventType } from "@/types/search";
import { BsPersonWalking } from "react-icons/bs";
import {
FaAmazon,
Expand Down Expand Up @@ -32,6 +33,7 @@ import {
GiRabbit,
GiRaccoonHead,
GiSailboat,
GiSoundWaves,
GiSquirrel,
} from "react-icons/gi";
import { LuBox, LuLassoSelect, LuScanBarcode } from "react-icons/lu";
Expand All @@ -56,11 +58,15 @@ export function isValidIconName(value: string): value is 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")) {
return getVerifiedIcon(label, className);
return getVerifiedIcon(label, className, type);
} else if (label.endsWith("-plate")) {
return getRecognizedPlateIcon(label, className);
return getRecognizedPlateIcon(label, className, type);
}

switch (label) {
Expand Down Expand Up @@ -152,27 +158,38 @@ export function getIconForLabel(label: string, className?: string) {
case "usps":
return <FaUsps key={label} className={className} />;
default:
if (type === "audio") {
return <GiSoundWaves 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("-"));

return (
<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" />
</div>
);
}

function getRecognizedPlateIcon(label: string, className?: string) {
function getRecognizedPlateIcon(
label: string,
className?: string,
type: EventType = "object",
) {
const simpleLabel = label.substring(0, label.lastIndexOf("-"));

return (
<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" />
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion web/src/views/classification/ModelTrainingView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
// title

useEffect(() => {
document.title = `${model.name} - ${t("documentTitle")}`;
document.title = `${model.name.toUpperCase()} - ${t("documentTitle")}`;
}, [model.name, t]);

// model state
Expand Down
4 changes: 2 additions & 2 deletions web/src/views/settings/ObjectSettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ function ObjectList({ cameraConfig, objects }: ObjectListProps) {
: getColorForObjectName(obj.label),
}}
>
{getIconForLabel(obj.label, "size-5 text-white")}
{getIconForLabel(obj.label, "object", "size-5 text-white")}
</div>
<div className="ml-3 text-lg">
{getTranslatedLabel(obj.label)}
Expand Down Expand Up @@ -494,7 +494,7 @@ function AudioList({ cameraConfig, audioDetections }: AudioListProps) {
<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="rounded-lg bg-selected p-2">
{getIconForLabel(key, "size-5 text-white")}
{getIconForLabel(key, "audio", "size-5 text-white")}
</div>
<div className="ml-3 text-lg">{getTranslatedLabel(key)}</div>
</div>
Expand Down