diff --git a/frigate/comms/webpush.py b/frigate/comms/webpush.py index a858f9eac..890226272 100644 --- a/frigate/comms/webpush.py +++ b/frigate/comms/webpush.py @@ -375,7 +375,19 @@ class WebPushClient(Communicator): ended = state == "end" or state == "genai" if state == "genai" and payload["after"]["data"]["metadata"]: - title = payload["after"]["data"]["metadata"]["title"] + base_title = payload["after"]["data"]["metadata"]["title"] + threat_level = payload["after"]["data"]["metadata"].get( + "potential_threat_level", 0 + ) + + # Add prefix for threat levels 1 and 2 + if threat_level == 1: + title = f"Needs Review: {base_title}" + elif threat_level == 2: + title = f"Security Concern: {base_title}" + else: + title = base_title + message = payload["after"]["data"]["metadata"]["scene"] else: title = f"{titlecase(', '.join(sorted_objects).replace('_', ' '))}{' was' if state == 'end' else ''} detected in {titlecase(', '.join(payload['after']['data']['zones']).replace('_', ' '))}" diff --git a/frigate/genai/__init__.py b/frigate/genai/__init__.py index dd42fc6dd..d86e3cbc5 100644 --- a/frigate/genai/__init__.py +++ b/frigate/genai/__init__.py @@ -205,14 +205,20 @@ Rules for the report: - Group bullets under subheadings when multiple events fall into the same category (e.g., Vehicle Activity, Porch Activity, Unusual Behavior). - Threat levels - - Always show (threat level: X) for each event. + - Always show the threat level for each event using these labels: + - Threat level 0: "Normal" + - Threat level 1: "Needs review" + - Threat level 2: "Security concern" + - Format as (threat level: Normal), (threat level: Needs review), or (threat level: Security concern). - If multiple events at the same time share the same threat level, only state it once. - Final assessment - End with a Final Assessment section. - - If all events are threat level 1 with no escalation: + - If all events are threat level 0: Final assessment: Only normal residential activity observed during this period. - - If threat level 2+ events are present, clearly summarize them as Potential concerns requiring review. + - If threat level 1 events are present: + Final assessment: Some activity requires review but no security concerns identified. + - If threat level 2 events are present, clearly summarize them as Security concerns requiring immediate attention. - Conciseness - Do not repeat benign clothing/appearance details unless they distinguish individuals. diff --git a/web/public/locales/en/views/events.json b/web/public/locales/en/views/events.json index d3cf78658..ee4aadef6 100644 --- a/web/public/locales/en/views/events.json +++ b/web/public/locales/en/views/events.json @@ -54,6 +54,7 @@ "selected_other": "{{count}} selected", "camera": "Camera", "detected": "detected", - "suspiciousActivity": "Suspicious Activity", - "threateningActivity": "Threatening Activity" + "normalActivity": "Normal", + "needsReview": "Needs review", + "securityConcern": "Security concern" } diff --git a/web/src/components/overlay/chip/GenAISummaryChip.tsx b/web/src/components/overlay/chip/GenAISummaryChip.tsx index 137eec88a..64eb4a10b 100644 --- a/web/src/components/overlay/chip/GenAISummaryChip.tsx +++ b/web/src/components/overlay/chip/GenAISummaryChip.tsx @@ -1,7 +1,11 @@ import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer"; import { cn } from "@/lib/utils"; -import { ReviewSegment, ThreatLevel } from "@/types/review"; +import { + ReviewSegment, + ThreatLevel, + THREAT_LEVEL_LABELS, +} from "@/types/review"; import { useEffect, useMemo, useState } from "react"; import { isDesktop } from "react-device-detect"; import { useTranslation } from "react-i18next"; @@ -55,13 +59,22 @@ export function GenAISummaryDialog({ } let concerns = ""; - switch (aiAnalysis.potential_threat_level) { - case ThreatLevel.SUSPICIOUS: - concerns = `• ${t("suspiciousActivity", { ns: "views/events" })}\n`; - break; - case ThreatLevel.DANGER: - concerns = `• ${t("threateningActivity", { ns: "views/events" })}\n`; - break; + const threatLevel = aiAnalysis.potential_threat_level ?? 0; + + if (threatLevel > 0) { + let label = ""; + + switch (threatLevel) { + case ThreatLevel.NEEDS_REVIEW: + label = t("needsReview", { ns: "views/events" }); + break; + case ThreatLevel.SECURITY_CONCERN: + label = t("securityConcern", { ns: "views/events" }); + break; + default: + label = THREAT_LEVEL_LABELS[threatLevel as ThreatLevel] || "Unknown"; + } + concerns = `• ${label}\n`; } (aiAnalysis.other_concerns ?? []).forEach((c) => { diff --git a/web/src/components/player/PreviewThumbnailPlayer.tsx b/web/src/components/player/PreviewThumbnailPlayer.tsx index ecf63760a..872f7c98a 100644 --- a/web/src/components/player/PreviewThumbnailPlayer.tsx +++ b/web/src/components/player/PreviewThumbnailPlayer.tsx @@ -1,7 +1,11 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; import { useApiHost } from "@/api"; import { isCurrentHour } from "@/utils/dateUtil"; -import { ReviewSegment } from "@/types/review"; +import { + ReviewSegment, + ThreatLevel, + THREAT_LEVEL_LABELS, +} from "@/types/review"; import { getIconForLabel } from "@/utils/iconUtil"; import TimeAgo from "../dynamic/TimeAgo"; import useSWR from "swr"; @@ -44,7 +48,7 @@ export default function PreviewThumbnailPlayer({ onClick, onTimeUpdate, }: PreviewPlayerProps) { - const { t } = useTranslation(["components/player"]); + const { t } = useTranslation(["components/player", "views/events"]); const apiHost = useApiHost(); const { data: config } = useSWR("config"); const [imgRef, imgLoaded, onImgLoad] = useImageLoaded(); @@ -319,11 +323,21 @@ export default function PreviewThumbnailPlayer({ - {review.data.metadata.potential_threat_level == 1 ? ( - <>{t("suspiciousActivity", { ns: "views/events" })} - ) : ( - <>{t("threateningActivity", { ns: "views/events" })} - )} + {(() => { + const threatLevel = + review.data.metadata.potential_threat_level ?? 0; + switch (threatLevel) { + case ThreatLevel.NEEDS_REVIEW: + return t("needsReview", { ns: "views/events" }); + case ThreatLevel.SECURITY_CONCERN: + return t("securityConcern", { ns: "views/events" }); + default: + return ( + THREAT_LEVEL_LABELS[threatLevel as ThreatLevel] || + "Unknown" + ); + } + })()} )} diff --git a/web/src/types/review.ts b/web/src/types/review.ts index 6c9027950..4e7b22334 100644 --- a/web/src/types/review.ts +++ b/web/src/types/review.ts @@ -87,6 +87,13 @@ export type ZoomLevel = { }; export enum ThreatLevel { - SUSPICIOUS = 1, - DANGER = 2, + NORMAL = 0, + NEEDS_REVIEW = 1, + SECURITY_CONCERN = 2, } + +export const THREAT_LEVEL_LABELS: Record = { + [ThreatLevel.NORMAL]: "Normal", + [ThreatLevel.NEEDS_REVIEW]: "Needs review", + [ThreatLevel.SECURITY_CONCERN]: "Security concern", +};