Compare commits

...

6 Commits

Author SHA1 Message Date
Nicolas Mowen
1bea9aec99 Formatting 2025-08-09 17:12:09 -06:00
Nicolas Mowen
374cd2bb2f Add log 2025-08-09 17:11:40 -06:00
Nicolas Mowen
8c511083ff Formatting 2025-08-09 13:39:46 -06:00
Nicolas Mowen
686fdef665 Adjust prompt 2025-08-09 13:38:41 -06:00
Nicolas Mowen
5876695aa1 Cleanup typing and UI 2025-08-09 13:34:37 -06:00
Nicolas Mowen
1452c61378 Remove message 2025-08-09 13:23:47 -06:00
5 changed files with 71 additions and 27 deletions

View File

@ -378,7 +378,9 @@ class WebPushClient(Communicator):
message = f"Detected on {titlecase(camera.replace('_', ' '))}"
if ended:
print(f"sending a message with message {message}")
logger.debug(
f"Sending a notification with state {state} and message {message}"
)
# if event is ongoing open to live view otherwise open to recordings view
direct_url = f"/review?id={reviewId}" if ended else f"/#{camera}"

View File

@ -41,31 +41,34 @@ class GenAIClient:
"""Generate a description for the review item activity."""
context_prompt = f"""
Please analyze the image(s), which are in chronological order, strictly from the perspective of the {review_data["camera"].replace("_", " ")} security camera.
Your task is to provide a **neutral, factual, and objective description** of the scene, while also including **reasonable, evidence-based inferences** about the likely context or activity — but do not make unfounded assumptions.
Your task is to provide a **neutral, factual, and objective description** of the scene, while also:
- Clearly stating **what is happening** based on observable actions and movements.
- Including **reasonable, evidence-based inferences** about the likely activity or context, but only if directly supported by visible details.
When forming your description:
- Base all statements on visible details in the images.
- You may deduce plausible intent or context only if supported by clear, observable evidence (e.g., someone carrying tools toward a car may indicate vehicle maintenance).
- Avoid implying hostility, criminal intent, or other strong judgments unless the visual evidence is unambiguous.
- Distinguish between what is certain (facts) and what appears likely (reasonable inference).
- **Facts first**: Describe the physical setting, people, and objects exactly as seen.
- **Then context**: Briefly note plausible purposes or activities (e.g., “appears to be delivering a package” if carrying a box to a door).
- Clearly separate certain facts (“A person is holding a ladder”) from reasonable inferences (“likely performing maintenance”).
- Do not speculate beyond what is visible, and do not imply hostility, criminal intent, or other strong judgments unless there is unambiguous visual evidence.
Here is some information we already know:
- The activity occurred at {review_data["timestamp"].strftime("%I:%M %p")}
- The following objects were detected: {review_data["objects"]}
- The following recognized objects were detected: {review_data["recognized_objects"]}
- The activity happened in the following zones: {review_data["zones"]}
Here is information already known:
- Activity occurred at {review_data["timestamp"].strftime("%I:%M %p")}
- Detected objects: {review_data["objects"]}
- Recognized objects: {review_data["recognized_objects"]}
- Zones involved: {review_data["zones"]}
Your response **MUST** be a flat JSON object with the following fields:
- `scene` (string): A comprehensive description of the setting and entities, including relevant context and plausible inferences if supported by visual evidence.
- `confidence` (float): A float between 0 and 1 representing your overall confidence in this analysis.
- `potential_threat_level` (integer, optional): 1–3 scale. Only include if a clear security concern is visible:
- 1 = Unusual but not overtly threatening
- 2 = Suspicious or potentially harmful
- 3 = Clear and immediate threat
Omit this field entirely if there is no observable security concern.
Your response **MUST** be a flat JSON object with:
- `scene` (string): A full description including setting, entities, actions, and any plausible supported inferences.
- `confidence` (float): A number 0–1 for overall confidence in the analysis.
- `potential_threat_level` (integer, optional): Include only if there is a clear, observable security concern:
- 1 = Unusual but not overtly threatening
- 2 = Suspicious or potentially harmful
- 3 = Clear and immediate threat
Omit this field if no concern is evident.
**IMPORTANT:**
Values for each field must be plain strings or integers — no nested objects or explanatory text.
- Values must be plain strings, floats, or integers — no nested objects, no extra commentary.
"""
response = self._send(context_prompt, thumbnails)
@ -76,8 +79,11 @@ class GenAIClient:
try:
return ReviewMetadata.model_validate_json(clean_json)
except Exception:
except Exception as e:
# rarely LLMs can fail to follow directions on output format
logger.warning(
f"Failed to parse review description as the response did not match expected format. {e}"
)
return None
else:
return None

View File

@ -11,7 +11,11 @@ import { FrigateConfig } from "@/types/frigateConfig";
import { useFormattedTimestamp } from "@/hooks/use-date-utils";
import { getIconForLabel } from "@/utils/iconUtil";
import { useApiHost } from "@/api";
import { ReviewDetailPaneType, ReviewSegment } from "@/types/review";
import {
ReviewDetailPaneType,
ReviewSegment,
ThreatLevel,
} from "@/types/review";
import { Event } from "@/types/event";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { cn } from "@/lib/utils";
@ -71,6 +75,23 @@ export default function ReviewDetailDialog({
const aiAnalysis = useMemo(() => review?.data?.metadata, [review]);
const aiThreatLevel = useMemo(() => {
if (!aiAnalysis?.potential_threat_level) {
return "None";
}
switch (aiAnalysis.potential_threat_level) {
case ThreatLevel.UNUSUAL:
return "Unusual Activity";
case ThreatLevel.SUSPICIOUS:
return "Suspicious Activity";
case ThreatLevel.DANGER:
return "Danger";
}
return "Unknown";
}, [aiAnalysis]);
const hasMismatch = useMemo(() => {
if (!review || !events) {
return false;
@ -235,12 +256,19 @@ export default function ReviewDetailDialog({
{pane == "overview" && (
<div className="flex flex-col gap-5 md:mt-3">
{aiAnalysis != undefined && (
<div className="m-2 flex h-full w-[90%] flex-col gap-2 rounded-md bg-card p-2">
<div
className={cn(
"m-2 flex h-full w-full flex-col gap-2 rounded-md bg-card p-2",
isDesktop && "w-[90%]",
)}
>
AI Analysis
<div className="text-sm text-primary/40">Description</div>
<div className="text-sm smart-capitalize">
{aiAnalysis.scene}
</div>
<div className="text-sm">{aiAnalysis.scene}</div>
<div className="text-sm text-primary/40">Score</div>
<div className="text-sm">{aiAnalysis.confidence * 100}%</div>
<div className="text-sm text-primary/40">Threat Level</div>
<div className="text-sm">{aiThreatLevel}</div>
</div>
)}
<div className="flex w-full flex-row">

View File

@ -20,6 +20,8 @@ export type ReviewData = {
zones: string[];
metadata?: {
scene: string;
confidence: number;
potential_threat_level?: number;
};
};
@ -76,3 +78,9 @@ export type ConsolidatedSegmentData = {
};
export type TimelineZoomDirection = "in" | "out" | null;
export enum ThreatLevel {
UNUSUAL = 1,
SUSPICIOUS = 2,
DANGER = 3,
}

View File

@ -4,7 +4,7 @@ import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import monacoEditorPlugin from "vite-plugin-monaco-editor";
const proxyHost = process.env.PROXY_HOST || "192.168.50.106:5002";
const proxyHost = process.env.PROXY_HOST || "localhost:5000";
// https://vitejs.dev/config/
export default defineConfig({