mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-06 05:24:11 +03:00
Review summary popup (#20383)
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
* Add title to prompt * Add popup for genai review summary * Add animation
This commit is contained in:
parent
33f0c23389
commit
7a8f93e9f5
@ -4,6 +4,7 @@ from pydantic import BaseModel, ConfigDict, Field
|
||||
class ReviewMetadata(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", protected_namespaces=())
|
||||
|
||||
title: str = Field(description="A concise title for the activity.")
|
||||
scene: str = Field(
|
||||
description="A comprehensive description of the setting and entities, including relevant context and plausible inferences if supported by visual evidence."
|
||||
)
|
||||
|
||||
@ -87,6 +87,7 @@ When forming your description:
|
||||
- **Weigh all evidence holistically**: Start by checking if the activity matches the normal patterns above. If it does, assign Level 0. Only consider Level 1 if the activity clearly deviates from normal patterns or shows genuine security concerns that warrant attention.
|
||||
|
||||
Your response MUST be a flat JSON object with:
|
||||
- `title` (string): A concise, one-sentence title that captures the main activity. Include any verified recognized objects (from the "Verified recognized objects" list below) and key detected objects. Examples: "Joe walking dog in backyard", "Unknown person testing car doors at night".
|
||||
- `scene` (string): A narrative description of what happens across the sequence from start to finish. **Only describe actions you can actually observe happening in the frames provided.** Do not infer or assume actions that aren't visible (e.g., if you see someone walking but never see them sit, don't say they sat down). Include setting, detected objects, and their observable actions. Avoid speculation or filling in assumed behaviors. Your description should align with and support the threat level you assign.
|
||||
- `confidence` (float): 0-1 confidence in your analysis. Higher confidence when objects/actions are clearly visible and context is unambiguous. Lower confidence when the sequence is unclear, objects are partially obscured, or context is ambiguous.
|
||||
- `potential_threat_level` (integer): 0, 1, or 2 as defined below. Your threat level must be consistent with your scene description and the guidance above.
|
||||
@ -167,7 +168,7 @@ Sequence details:
|
||||
timeline_summary_prompt = f"""
|
||||
You are a security officer.
|
||||
Time range: {time_range}.
|
||||
Input: JSON list with "scene", "confidence", "potential_threat_level" (1-2), "other_concerns".
|
||||
Input: JSON list with "title", "scene", "confidence", "potential_threat_level" (1-2), "other_concerns".
|
||||
|
||||
Task: Write a concise, human-presentable security report in markdown format.
|
||||
|
||||
|
||||
117
web/src/components/overlay/chip/GenAISummaryChip.tsx
Normal file
117
web/src/components/overlay/chip/GenAISummaryChip.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
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 { useEffect, useMemo, useState } from "react";
|
||||
import { isDesktop } from "react-device-detect";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MdAutoAwesome } from "react-icons/md";
|
||||
|
||||
type GenAISummaryChipProps = {
|
||||
review?: ReviewSegment;
|
||||
onClick: () => void;
|
||||
};
|
||||
export function GenAISummaryChip({ review, onClick }: GenAISummaryChipProps) {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsVisible(review?.data?.metadata != undefined);
|
||||
}, [review]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute left-1/2 top-8 z-30 flex max-w-[90vw] -translate-x-[50%] cursor-pointer select-none items-center gap-2 rounded-full bg-card p-2 text-sm transition-all duration-500",
|
||||
isVisible ? "translate-y-0 opacity-100" : "-translate-y-4 opacity-0",
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<MdAutoAwesome className="shrink-0" />
|
||||
<span className="truncate">{review?.data.metadata?.title}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type GenAISummaryDialogProps = {
|
||||
review?: ReviewSegment;
|
||||
onOpen?: (open: boolean) => void;
|
||||
};
|
||||
export function GenAISummaryDialog({
|
||||
review,
|
||||
onOpen,
|
||||
}: GenAISummaryDialogProps) {
|
||||
const { t } = useTranslation(["views/explore"]);
|
||||
|
||||
// data
|
||||
|
||||
const aiAnalysis = useMemo(() => review?.data?.metadata, [review]);
|
||||
const aiThreatLevel = useMemo(() => {
|
||||
if (
|
||||
!aiAnalysis ||
|
||||
(!aiAnalysis.potential_threat_level && !aiAnalysis.other_concerns)
|
||||
) {
|
||||
return "None";
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
(aiAnalysis.other_concerns ?? []).forEach((c) => {
|
||||
concerns += `• ${c}\n`;
|
||||
});
|
||||
|
||||
return concerns || "None";
|
||||
}, [aiAnalysis, t]);
|
||||
|
||||
// layout
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const Overlay = isDesktop ? Dialog : Drawer;
|
||||
const Trigger = isDesktop ? DialogTrigger : DrawerTrigger;
|
||||
const Content = isDesktop ? DialogContent : DrawerContent;
|
||||
|
||||
useEffect(() => {
|
||||
if (onOpen) {
|
||||
onOpen(open);
|
||||
}
|
||||
}, [open, onOpen]);
|
||||
|
||||
if (!aiAnalysis) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Overlay open={open} onOpenChange={setOpen}>
|
||||
<Trigger asChild>
|
||||
<GenAISummaryChip review={review} onClick={() => setOpen(true)} />
|
||||
</Trigger>
|
||||
<Content
|
||||
className={cn(
|
||||
"gap-2",
|
||||
isDesktop
|
||||
? "sm:rounded-lg md:rounded-2xl"
|
||||
: "mx-4 rounded-lg px-4 pb-4 md:rounded-2xl",
|
||||
)}
|
||||
>
|
||||
{t("aiAnalysis.title")}
|
||||
<div className="text-sm text-primary/40">
|
||||
{t("details.description.label")}
|
||||
</div>
|
||||
<div className="text-sm">{aiAnalysis.scene}</div>
|
||||
<div className="text-sm text-primary/40">
|
||||
{t("details.score.label")}
|
||||
</div>
|
||||
<div className="text-sm">{aiAnalysis.confidence * 100}%</div>
|
||||
<div className="text-sm text-primary/40">{t("concerns.label")}</div>
|
||||
<div className="text-sm">{aiThreatLevel}</div>
|
||||
</Content>
|
||||
</Overlay>
|
||||
);
|
||||
}
|
||||
@ -19,6 +19,7 @@ export type ReviewData = {
|
||||
significant_motion_areas: number[];
|
||||
zones: string[];
|
||||
metadata?: {
|
||||
title: string;
|
||||
scene: string;
|
||||
confidence: number;
|
||||
potential_threat_level?: number;
|
||||
|
||||
@ -66,6 +66,7 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { CameraNameLabel } from "@/components/camera/CameraNameLabel";
|
||||
import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
|
||||
import { GenAISummaryDialog } from "@/components/overlay/chip/GenAISummaryChip";
|
||||
|
||||
type RecordingViewProps = {
|
||||
startCamera: string;
|
||||
@ -458,6 +459,29 @@ export function RecordingView({
|
||||
[visiblePreviewObserver.current],
|
||||
);
|
||||
|
||||
const activeReviewItem = useMemo(() => {
|
||||
if (!config?.cameras?.[mainCamera].review.genai?.enabled_in_config) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return mainCameraReviewItems.find(
|
||||
(rev) =>
|
||||
rev.start_time < currentTime &&
|
||||
rev.end_time &&
|
||||
currentTime < rev.end_time,
|
||||
);
|
||||
}, [config, currentTime, mainCameraReviewItems, mainCamera]);
|
||||
const onAnalysisOpen = useCallback(
|
||||
(open: boolean) => {
|
||||
if (open) {
|
||||
mainControllerRef.current?.pause();
|
||||
} else {
|
||||
mainControllerRef.current?.play();
|
||||
}
|
||||
},
|
||||
[mainControllerRef],
|
||||
);
|
||||
|
||||
return (
|
||||
<div ref={contentRef} className="flex size-full flex-col pt-2">
|
||||
<Toaster closeButton={true} />
|
||||
@ -654,6 +678,11 @@ export function RecordingView({
|
||||
: Math.max(1, getCameraAspect(mainCamera) ?? 0),
|
||||
}}
|
||||
>
|
||||
<GenAISummaryDialog
|
||||
review={activeReviewItem}
|
||||
onOpen={onAnalysisOpen}
|
||||
/>
|
||||
|
||||
<DynamicVideoPlayer
|
||||
className={grow}
|
||||
camera={mainCamera}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user