mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-06 05:24:11 +03:00
320 lines
16 KiB
Python
320 lines
16 KiB
Python
"""Generative AI module for Frigate."""
|
|
|
|
import datetime
|
|
import importlib
|
|
import logging
|
|
import os
|
|
import re
|
|
from typing import Any, Optional
|
|
|
|
from playhouse.shortcuts import model_to_dict
|
|
|
|
from frigate.config import CameraConfig, FrigateConfig, GenAIConfig, GenAIProviderEnum
|
|
from frigate.const import CLIPS_DIR
|
|
from frigate.data_processing.post.types import ReviewMetadata
|
|
from frigate.models import Event
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
PROVIDERS = {}
|
|
|
|
|
|
def register_genai_provider(key: GenAIProviderEnum):
|
|
"""Register a GenAI provider."""
|
|
|
|
def decorator(cls):
|
|
PROVIDERS[key] = cls
|
|
return cls
|
|
|
|
return decorator
|
|
|
|
|
|
class GenAIClient:
|
|
"""Generative AI client for Frigate."""
|
|
|
|
def __init__(self, genai_config: GenAIConfig, timeout: int = 120) -> None:
|
|
self.genai_config: GenAIConfig = genai_config
|
|
self.timeout = timeout
|
|
self.provider = self._init_provider()
|
|
|
|
def generate_review_description(
|
|
self,
|
|
review_data: dict[str, Any],
|
|
thumbnails: list[bytes],
|
|
concerns: list[str],
|
|
preferred_language: str | None,
|
|
debug_save: bool,
|
|
activity_context_prompt: str,
|
|
) -> ReviewMetadata | None:
|
|
"""Generate a description for the review item activity."""
|
|
|
|
def get_concern_prompt() -> str:
|
|
if concerns:
|
|
concern_list = "\n - ".join(concerns)
|
|
return f"""- `other_concerns` (list of strings): Include a list of any of the following concerns that are occurring:
|
|
- {concern_list}"""
|
|
else:
|
|
return ""
|
|
|
|
def get_language_prompt() -> str:
|
|
if preferred_language:
|
|
return f"Provide your answer in {preferred_language}"
|
|
else:
|
|
return ""
|
|
|
|
def get_objects_list() -> str:
|
|
if review_data["unified_objects"]:
|
|
return "\n- " + "\n- ".join(review_data["unified_objects"])
|
|
else:
|
|
return "\n- (No objects detected)"
|
|
|
|
context_prompt = f"""
|
|
Your task is to analyze the sequence of images ({len(thumbnails)} total) taken in chronological order from the perspective of the {review_data["camera"]} security camera.
|
|
|
|
## Normal Activity Patterns for This Property
|
|
|
|
{activity_context_prompt}
|
|
|
|
## Task Instructions
|
|
|
|
Your task is to provide a clear, accurate description of the scene that:
|
|
1. States exactly what is happening based on observable actions and movements.
|
|
2. Evaluates the activity against the Normal and Suspicious Activity Indicators above.
|
|
3. Assigns a potential_threat_level (0, 1, or 2) based on the threat level indicators defined above, applying them consistently.
|
|
|
|
**Use the activity patterns above as guidance to calibrate your assessment. Match the activity against both normal and suspicious indicators, then use your judgment based on the complete context.**
|
|
|
|
## Analysis Guidelines
|
|
|
|
When forming your description:
|
|
- **CRITICAL: Only describe objects explicitly listed in "Objects in Scene" below.** Do not infer or mention additional people, vehicles, or objects not present in this list, even if visual patterns suggest them. If only a car is listed, do not describe a person interacting with it unless "person" is also in the objects list.
|
|
- **Only describe actions actually visible in the frames.** Do not assume or infer actions that you don't observe happening. If someone walks toward furniture but you never see them sit, do not say they sat. Stick to what you can see across the sequence.
|
|
- Describe what you observe: actions, movements, interactions with objects and the environment. Include any observable environmental changes (e.g., lighting changes triggered by activity).
|
|
- Note visible details such as clothing, items being carried or placed, tools or equipment present, and how they interact with the property or objects.
|
|
- Consider the full sequence chronologically: what happens from start to finish, how duration and actions relate to the location and objects involved.
|
|
- **Use the actual timestamp provided in "Activity started at"** below for time of day context—do not infer time from image brightness or darkness. Unusual hours (late night/early morning) should increase suspicion when the observable behavior itself appears questionable. However, recognize that some legitimate activities can occur at any hour.
|
|
- **Consider duration as a primary factor**: Apply the duration thresholds defined in the activity patterns above. Brief sequences during normal hours with apparent purpose typically indicate normal activity unless explicit suspicious actions are visible.
|
|
- **Weigh all evidence holistically**: Match the activity against the normal and suspicious patterns defined above, then evaluate based on the complete context (zone, objects, time, actions, duration). Apply the threat level indicators consistently. Use your judgment for edge cases.
|
|
|
|
## Response Format
|
|
|
|
Your response MUST be a flat JSON object with:
|
|
- `title` (string): A concise, direct title that describes the primary action or event in the sequence, not just what you literally see. Use spatial context when available to make titles more meaningful. When multiple objects/actions are present, prioritize whichever is most prominent or occurs first. Use names from "Objects in Scene" based on what you visually observe. If you see both a name and an unidentified object of the same type but visually observe only one person/object, use ONLY the name. Examples: "Joe walking dog", "Person taking out trash", "Vehicle arriving in driveway", "Joe accessing vehicle", "Person leaving porch for driveway".
|
|
- `scene` (string): A narrative description of what happens across the sequence from start to finish, in chronological order. Start by describing how the sequence begins, then describe the progression of events. **Describe all significant movements and actions in the order they occur.** For example, if a vehicle arrives and then a person exits, describe both actions sequentially. **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 in "Normal Activity Patterns for This Property" above. Your threat level must be consistent with your scene description and the guidance above.
|
|
{get_concern_prompt()}
|
|
|
|
## Sequence Details
|
|
|
|
- Frame 1 = earliest, Frame {len(thumbnails)} = latest
|
|
- Activity started at {review_data["start"]} and lasted {review_data["duration"]} seconds
|
|
- Zones involved: {", ".join(review_data["zones"]) if review_data["zones"] else "None"}
|
|
|
|
## Objects in Scene
|
|
|
|
Each line represents a detection state, not necessarily unique individuals. Parentheses indicate object type or category, use only the name/label in your response, not the parentheses.
|
|
|
|
**CRITICAL: When you see both recognized and unrecognized entries of the same type (e.g., "Joe (person)" and "Person"), visually count how many distinct people/objects you actually see based on appearance and clothing. If you observe only ONE person throughout the sequence, use ONLY the recognized name (e.g., "Joe"). The same person may be recognized in some frames but not others. Only describe both if you visually see MULTIPLE distinct people with clearly different appearances.**
|
|
|
|
**Note: Unidentified objects (without names) are NOT indicators of suspicious activity—they simply mean the system hasn't identified that object.**
|
|
{get_objects_list()}
|
|
|
|
## Important Notes
|
|
- Values must be plain strings, floats, or integers — no nested objects, no extra commentary.
|
|
- Only describe objects from the "Objects in Scene" list above. Do not hallucinate additional objects.
|
|
- When describing people or vehicles, use the exact names provided.
|
|
{get_language_prompt()}
|
|
"""
|
|
logger.debug(
|
|
f"Sending {len(thumbnails)} images to create review description on {review_data['camera']}"
|
|
)
|
|
|
|
if debug_save:
|
|
with open(
|
|
os.path.join(
|
|
CLIPS_DIR, "genai-requests", review_data["id"], "prompt.txt"
|
|
),
|
|
"w",
|
|
) as f:
|
|
f.write(context_prompt)
|
|
|
|
response = self._send(context_prompt, thumbnails)
|
|
|
|
if debug_save and response:
|
|
with open(
|
|
os.path.join(
|
|
CLIPS_DIR, "genai-requests", review_data["id"], "response.txt"
|
|
),
|
|
"w",
|
|
) as f:
|
|
f.write(response)
|
|
|
|
if response:
|
|
clean_json = re.sub(
|
|
r"\n?```$", "", re.sub(r"^```[a-zA-Z0-9]*\n?", "", response)
|
|
)
|
|
|
|
try:
|
|
metadata = ReviewMetadata.model_validate_json(clean_json)
|
|
|
|
# If any verified objects (contain parentheses with name), set to 0
|
|
if any("(" in obj for obj in review_data["unified_objects"]):
|
|
metadata.potential_threat_level = 0
|
|
|
|
metadata.time = review_data["start"]
|
|
return metadata
|
|
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
|
|
|
|
def generate_review_summary(
|
|
self,
|
|
start_ts: float,
|
|
end_ts: float,
|
|
segments: list[dict[str, Any]],
|
|
debug_save: bool,
|
|
) -> str | None:
|
|
"""Generate a summary of review item descriptions over a period of time."""
|
|
time_range = f"{datetime.datetime.fromtimestamp(start_ts).strftime('%B %d, %Y at %I:%M %p')} to {datetime.datetime.fromtimestamp(end_ts).strftime('%B %d, %Y at %I:%M %p')}"
|
|
timeline_summary_prompt = f"""
|
|
You are a security officer.
|
|
Time range: {time_range}.
|
|
Input: JSON list with "title", "scene", "confidence", "potential_threat_level" (0-2), "other_concerns", "_is_primary", "_camera".
|
|
|
|
Task: Write a concise, human-presentable security report in markdown format.
|
|
|
|
CRITICAL - Understanding Primary vs Contextual Items:
|
|
- Items with "_is_primary": true are events that REQUIRE REVIEW and MUST be included in the report
|
|
- Items with "_is_primary": false are additional context from other camera perspectives
|
|
- **DO NOT create separate bullet points or sections for contextual items**
|
|
- **ONLY use contextual items to enrich and inform the description of primary items**
|
|
- The "_camera" field indicates which camera captured each event
|
|
- When a contextual item provides relevant background (e.g., delivery truck explaining person at door), incorporate that information directly into the primary event's description
|
|
|
|
Rules for the report:
|
|
|
|
- Title & overview
|
|
- Start with:
|
|
# Security Summary - {time_range}
|
|
- Write a 1-2 sentence situational overview capturing the general pattern of the period.
|
|
|
|
- Event details
|
|
- **ONLY create bullet points for PRIMARY items (_is_primary: true)**
|
|
- Do NOT create separate bullets for contextual items
|
|
- Present primary events in chronological order as a bullet list.
|
|
- When contextual items overlap with a primary event, weave that information naturally into the primary event's description.
|
|
- Example: Instead of "Person at door" write "Person at door (delivery driver, as delivery truck was present on driveway camera)"
|
|
- If multiple PRIMARY events occur within the same minute, combine them into a single bullet with sub-points.
|
|
- If no timestamp is given, preserve order but label as "Time not specified."
|
|
- Use bold timestamps for clarity.
|
|
- Include the primary event's camera name.
|
|
- Group bullets under subheadings when multiple PRIMARY events fall into the same category (e.g., Porch Activity, Unusual Behavior).
|
|
|
|
- Threat levels
|
|
- Always show the threat level for PRIMARY events 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).
|
|
- When contextual items explain or reduce concern about a primary event, the description should reflect this understanding.
|
|
- If multiple PRIMARY events at the same time share the same threat level, only state it once.
|
|
|
|
- Final assessment
|
|
- End with a Final Assessment section.
|
|
- If all primary events are threat level 0 or explained by contextual items:
|
|
Final assessment: Only normal residential activity observed during this period.
|
|
- 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.
|
|
- Keep this section brief - do not repeat details from the event descriptions above.
|
|
|
|
- Conciseness
|
|
- Do not repeat benign clothing/appearance details unless they distinguish individuals.
|
|
- Summarize similar routine events instead of restating full scene descriptions.
|
|
- When incorporating contextual information, do so briefly and naturally within the primary event description.
|
|
- Avoid lengthy explanatory notes - integrate context seamlessly into the narrative.
|
|
"""
|
|
|
|
for item in segments:
|
|
timeline_summary_prompt += f"\n{item}"
|
|
|
|
if debug_save:
|
|
with open(
|
|
os.path.join(
|
|
CLIPS_DIR, "genai-requests", f"{start_ts}-{end_ts}", "prompt.txt"
|
|
),
|
|
"w",
|
|
) as f:
|
|
f.write(timeline_summary_prompt)
|
|
|
|
response = self._send(timeline_summary_prompt, [])
|
|
|
|
if debug_save and response:
|
|
with open(
|
|
os.path.join(
|
|
CLIPS_DIR, "genai-requests", f"{start_ts}-{end_ts}", "response.txt"
|
|
),
|
|
"w",
|
|
) as f:
|
|
f.write(response)
|
|
|
|
return response
|
|
|
|
def generate_object_description(
|
|
self,
|
|
camera_config: CameraConfig,
|
|
thumbnails: list[bytes],
|
|
event: Event,
|
|
) -> Optional[str]:
|
|
"""Generate a description for the frame."""
|
|
try:
|
|
prompt = camera_config.objects.genai.object_prompts.get(
|
|
event.label,
|
|
camera_config.objects.genai.prompt,
|
|
).format(**model_to_dict(event))
|
|
except KeyError as e:
|
|
logger.error(f"Invalid key in GenAI prompt: {e}")
|
|
return None
|
|
|
|
logger.debug(f"Sending images to genai provider with prompt: {prompt}")
|
|
return self._send(prompt, thumbnails)
|
|
|
|
def _init_provider(self):
|
|
"""Initialize the client."""
|
|
return None
|
|
|
|
def _send(self, prompt: str, images: list[bytes]) -> Optional[str]:
|
|
"""Submit a request to the provider."""
|
|
return None
|
|
|
|
def get_context_size(self) -> int:
|
|
"""Get the context window size for this provider in tokens."""
|
|
return 4096
|
|
|
|
|
|
def get_genai_client(config: FrigateConfig) -> Optional[GenAIClient]:
|
|
"""Get the GenAI client."""
|
|
if not config.genai.provider:
|
|
return None
|
|
|
|
load_providers()
|
|
provider = PROVIDERS.get(config.genai.provider)
|
|
if provider:
|
|
return provider(config.genai)
|
|
|
|
return None
|
|
|
|
|
|
def load_providers():
|
|
package_dir = os.path.dirname(__file__)
|
|
for filename in os.listdir(package_dir):
|
|
if filename.endswith(".py") and filename != "__init__.py":
|
|
module_name = f"frigate.genai.{filename[:-3]}"
|
|
importlib.import_module(module_name)
|