2024-06-22 00:30:19 +03:00
""" Generative AI module for Frigate. """
2025-08-13 01:27:35 +03:00
import datetime
2024-06-22 00:30:19 +03:00
import importlib
2024-11-16 00:24:17 +03:00
import logging
2024-06-22 00:30:19 +03:00
import os
2025-08-10 14:57:54 +03:00
import re
from typing import Any , Optional
2024-06-22 00:30:19 +03:00
2026-03-08 18:55:00 +03:00
import numpy as np
2024-10-14 15:23:10 +03:00
from playhouse . shortcuts import model_to_dict
2026-02-27 18:35:33 +03:00
from frigate . config import CameraConfig , GenAIConfig , GenAIProviderEnum
2025-08-13 01:27:35 +03:00
from frigate . const import CLIPS_DIR
2025-08-10 14:57:54 +03:00
from frigate . data_processing . post . types import ReviewMetadata
2026-02-27 18:35:33 +03:00
from frigate . genai . manager import GenAIClientManager
2024-10-12 15:19:24 +03:00
from frigate . models import Event
2024-06-22 00:30:19 +03:00
2024-11-16 00:24:17 +03:00
logger = logging . getLogger ( __name__ )
2026-02-27 18:35:33 +03:00
__all__ = [
" GenAIClient " ,
" GenAIClientManager " ,
" GenAIConfig " ,
" GenAIProviderEnum " ,
" PROVIDERS " ,
" load_providers " ,
" register_genai_provider " ,
]
2024-06-22 00:30:19 +03:00
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. """
2025-10-02 21:48:11 +03:00
def __init__ ( self , genai_config : GenAIConfig , timeout : int = 120 ) - > None :
2024-06-22 00:30:19 +03:00
self . genai_config : GenAIConfig = genai_config
self . timeout = timeout
self . provider = self . _init_provider ( )
2025-08-10 14:57:54 +03:00
def generate_review_description (
2025-08-11 22:17:25 +03:00
self ,
review_data : dict [ str , Any ] ,
thumbnails : list [ bytes ] ,
concerns : list [ str ] ,
preferred_language : str | None ,
2025-08-13 01:27:35 +03:00
debug_save : bool ,
2025-10-01 02:07:16 +03:00
activity_context_prompt : str ,
2025-08-10 14:57:54 +03:00
) - > ReviewMetadata | None :
""" Generate a description for the review item activity. """
2025-08-11 22:17:25 +03:00
2025-08-13 18:28:01 +03:00
def get_concern_prompt ( ) - > str :
if concerns :
concern_list = " \n - " . join ( concerns )
2025-11-10 20:03:56 +03:00
return f """ - `other_concerns` (list of strings): Include a list of any of the following concerns that are occurring:
2025-08-13 18:28:01 +03:00
- { concern_list } """
else :
return " "
2025-08-11 22:17:25 +03:00
2025-08-13 18:28:01 +03:00
def get_language_prompt ( ) - > str :
if preferred_language :
return f " Provide your answer in { preferred_language } "
else :
return " "
2025-08-11 22:17:25 +03:00
2025-10-27 00:37:57 +03:00
def get_objects_list ( ) - > str :
if review_data [ " unified_objects " ] :
return " \n - " + " \n - " . join ( review_data [ " unified_objects " ] )
2025-10-10 16:07:00 +03:00
else :
2025-10-27 00:37:57 +03:00
return " \n - (No objects detected) "
2025-10-10 16:07:00 +03:00
2025-08-10 14:57:54 +03:00
context_prompt = f """
2026-01-22 22:04:40 +03:00
Your task is to analyze a sequence of images taken in chronological order from a security camera .
2025-08-11 22:17:25 +03:00
2025-10-26 00:40:04 +03:00
## Normal Activity Patterns for This Property
2025-10-27 00:37:57 +03:00
2025-10-02 18:17:25 +03:00
{ activity_context_prompt }
2025-10-26 00:40:04 +03:00
## Task Instructions
2025-09-30 15:52:38 +03:00
Your task is to provide a clear , accurate description of the scene that :
2025-08-13 01:27:35 +03:00
1. States exactly what is happening based on observable actions and movements .
2025-10-27 19:34:39 +03:00
2. Evaluates the activity against the Normal and Suspicious Activity Indicators above .
2025-10-30 17:52:55 +03:00
3. Assigns a potential_threat_level ( 0 , 1 , or 2 ) based on the threat level indicators defined above , applying them consistently .
2025-08-11 22:17:25 +03:00
2025-10-27 19:34:39 +03:00
* * 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 . * *
2025-08-11 22:17:25 +03:00
2025-10-26 00:40:04 +03:00
## Analysis Guidelines
2025-08-13 01:27:35 +03:00
When forming your description :
2025-10-27 00:37:57 +03:00
- * * 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 .
2025-09-30 15:52:38 +03:00
- * * 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 .
2025-10-01 02:07:16 +03:00
- 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 .
2025-10-30 17:52:55 +03:00
- * * 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 .
2025-10-01 02:07:16 +03:00
2025-10-26 00:40:04 +03:00
## Response Format
2025-08-13 01:27:35 +03:00
Your response MUST be a flat JSON object with :
2025-11-08 15:44:30 +03:00
- ` 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 .
2026-02-03 17:31:00 +03:00
- ` title ` ( string ) : A concise , grammatically complete title in the format " [Subject] [action verb] [context] " that matches your scene description . Use names from " Objects in Scene " when you visually observe them .
2025-12-26 17:45:03 +03:00
- ` shortSummary ` ( string ) : A brief 2 - sentence summary of the scene , suitable for notifications . Should capture the key activity and context without full detail . This should be a condensed version of the scene description above .
2025-09-30 15:52:38 +03:00
- ` 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 .
2025-10-30 17:52:55 +03:00
- ` 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 .
2025-08-13 18:28:01 +03:00
{ get_concern_prompt ( ) }
2025-08-11 22:17:25 +03:00
2025-10-26 00:40:04 +03:00
## Sequence Details
2026-01-22 22:04:40 +03:00
- Camera : { review_data [ " camera " ] }
- Total frames : { len ( thumbnails ) } ( Frame 1 = earliest , Frame { len ( thumbnails ) } = latest )
2025-08-15 16:25:49 +03:00
- Activity started at { review_data [ " start " ] } and lasted { review_data [ " duration " ] } seconds
2025-11-10 20:03:56 +03:00
- Zones involved : { " , " . join ( review_data [ " zones " ] ) if review_data [ " zones " ] else " None " }
2025-08-13 01:27:35 +03:00
2025-10-27 00:37:57 +03:00
## Objects in Scene
2025-10-31 21:40:31 +03:00
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 .
2025-10-28 16:28:36 +03:00
2025-10-29 17:40:50 +03:00
* * 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 . * *
2025-10-28 16:28:36 +03:00
2025-10-29 17:40:50 +03:00
* * Note : Unidentified objects ( without names ) are NOT indicators of suspicious activity — they simply mean the system hasn ' t identified that object.**
2025-10-27 00:37:57 +03:00
{ get_objects_list ( ) }
2025-10-26 00:40:04 +03:00
## Important Notes
2025-08-11 22:17:25 +03:00
- Values must be plain strings , floats , or integers — no nested objects , no extra commentary .
2025-10-27 00:37:57 +03:00
- 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 .
2025-08-13 18:28:01 +03:00
{ get_language_prompt ( ) }
2025-09-26 05:05:22 +03:00
"""
2025-08-10 19:24:08 +03:00
logger . debug (
f " Sending { len ( thumbnails ) } images to create review description on { review_data [ ' camera ' ] } "
)
2025-08-13 01:27:35 +03:00
if debug_save :
with open (
os . path . join (
CLIPS_DIR , " genai-requests " , review_data [ " id " ] , " prompt.txt "
) ,
" w " ,
) as f :
f . write ( context_prompt )
2026-01-29 21:30:21 +03:00
response = self . _send ( context_prompt , thumbnails )
2025-08-10 14:57:54 +03:00
2025-08-19 15:49:55 +03:00
if debug_save and response :
2025-08-15 16:25:49 +03:00
with open (
os . path . join (
CLIPS_DIR , " genai-requests " , review_data [ " id " ] , " response.txt "
) ,
" w " ,
) as f :
f . write ( response )
2025-08-10 14:57:54 +03:00
if response :
clean_json = re . sub (
r " \ n?```$ " , " " , re . sub ( r " ^```[a-zA-Z0-9]* \ n? " , " " , response )
)
try :
2025-08-15 16:25:49 +03:00
metadata = ReviewMetadata . model_validate_json ( clean_json )
2025-10-29 17:40:50 +03:00
# If any verified objects (contain parentheses with name), set to 0
if any ( " ( " in obj for obj in review_data [ " unified_objects " ] ) :
2025-08-15 16:25:49 +03:00
metadata . potential_threat_level = 0
2025-09-26 05:05:22 +03:00
metadata . time = review_data [ " start " ]
2025-08-15 16:25:49 +03:00
return metadata
2025-08-10 14:57:54 +03:00
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
2025-08-13 01:27:35 +03:00
def generate_review_summary (
2025-09-26 05:05:22 +03:00
self ,
start_ts : float ,
end_ts : float ,
2025-12-11 17:23:34 +03:00
events : list [ dict [ str , Any ] ] ,
2025-12-21 03:30:34 +03:00
preferred_language : str | None ,
2025-09-26 05:05:22 +03:00
debug_save : bool ,
2025-08-13 01:27:35 +03:00
) - > str | None :
""" Generate a summary of review item descriptions over a period of time. """
2025-09-26 05:05:22 +03:00
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 ' ) } "
2025-08-13 01:27:35 +03:00
timeline_summary_prompt = f """
2025-12-11 17:23:34 +03:00
You are a security officer writing a concise security report .
Time range : { time_range }
Input format : Each event is a JSON object with :
- " title " , " scene " , " confidence " , " potential_threat_level " ( 0 - 2 ) , " other_concerns " , " camera " , " time " , " start_time " , " end_time "
- " context " : array of related events from other cameras that occurred during overlapping time periods
2025-12-26 17:45:03 +03:00
* * Note : Use the " scene " field for event descriptions in the report . Ignore any " shortSummary " field if present . * *
2025-12-11 17:23:34 +03:00
Report Structure - Use this EXACT format :
# Security Summary - {time_range}
## Overview
[ Write 1 - 2 sentences summarizing the overall activity pattern during this period . ]
- - -
## Timeline
[ Group events by time periods ( e . g . , " Morning (6:00 AM - 12:00 PM) " , " Afternoon (12:00 PM - 5:00 PM) " , " Evening (5:00 PM - 9:00 PM) " , " Night (9:00 PM - 6:00 AM) " ) . Use appropriate time blocks based on when events occurred . ]
### [Time Block Name]
* * HH : MM AM / PM * * | [ Camera Name ] | [ Threat Level Indicator ]
- [ Event title ] : [ Clear description incorporating contextual information from the " context " array ]
- Context : [ If context array has items , mention them here , e . g . , " Delivery truck present on Front Driveway Cam (HH:MM AM/PM) " ]
- Assessment : [ Brief assessment incorporating context - if context explains the event , note it here ]
[ Repeat for each event in chronological order within the time block ]
- - -
## Summary
[ One sentence summarizing the period . If all events are normal / explained : " Routine activity observed. " If review needed : " Some activity requires review but no security concerns. " If security concerns : " Security concerns requiring immediate attention. " ]
Guidelines :
- List ALL events in chronological order , grouped by time blocks
- Threat level indicators : ✓ Normal , ⚠ ️ Needs review , 🔴 Security concern
- Integrate contextual information naturally - use the " context " array to enrich each event ' s description
- If context explains the event ( e . g . , delivery truck explains person at door ) , describe it accordingly ( e . g . , " delivery person " not " unidentified person " )
- Be concise but informative - focus on what happened and what it means
- If contextual information makes an event clearly normal , reflect that in your assessment
- Only create time blocks that have events - don ' t create empty sections
2025-09-26 05:05:22 +03:00
"""
2025-08-13 01:27:35 +03:00
2025-12-11 17:23:34 +03:00
timeline_summary_prompt + = " \n \n Events: \n "
for event in events :
timeline_summary_prompt + = f " \n { event } \n "
2025-08-13 01:27:35 +03:00
2025-12-21 03:30:34 +03:00
if preferred_language :
timeline_summary_prompt + = f " \n Provide your answer in { preferred_language } "
2025-09-26 05:05:22 +03:00
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
2025-08-13 01:27:35 +03:00
2025-08-10 14:57:54 +03:00
def generate_object_description (
2024-09-16 17:46:11 +03:00
self ,
camera_config : CameraConfig ,
thumbnails : list [ bytes ] ,
2024-10-12 15:19:24 +03:00
event : Event ,
2024-06-22 00:30:19 +03:00
) - > Optional [ str ] :
""" Generate a description for the frame. """
2025-08-20 16:03:50 +03:00
try :
2025-10-02 14:48:16 +03:00
prompt = camera_config . objects . genai . object_prompts . get (
2025-08-20 16:03:50 +03:00
event . label ,
2025-10-02 14:48:16 +03:00
camera_config . objects . genai . prompt ,
2025-08-20 16:03:50 +03:00
) . format ( * * model_to_dict ( event ) )
except KeyError as e :
logger . error ( f " Invalid key in GenAI prompt: { e } " )
return None
2024-11-16 00:24:17 +03:00
logger . debug ( f " Sending images to genai provider with prompt: { prompt } " )
2024-06-22 00:30:19 +03:00
return self . _send ( prompt , thumbnails )
def _init_provider ( self ) :
""" Initialize the client. """
return None
2026-01-29 21:30:21 +03:00
def _send ( self , prompt : str , images : list [ bytes ] ) - > Optional [ str ] :
""" Submit a request to the provider. """
2024-06-22 00:30:19 +03:00
return None
2025-10-02 18:17:25 +03:00
def get_context_size ( self ) - > int :
""" Get the context window size for this provider in tokens. """
return 4096
2026-03-08 18:55:00 +03:00
def embed (
self ,
texts : list [ str ] | None = None ,
images : list [ bytes ] | None = None ,
) - > list [ np . ndarray ] :
""" Generate embeddings for text and/or images.
Returns list of numpy arrays ( one per input ) . Expected dimension is 768
for Frigate semantic search compatibility .
Providers that support embeddings should override this method .
"""
logger . warning (
" %s does not support embeddings. "
" This method should be overridden by the provider implementation. " ,
self . __class__ . __name__ ,
)
return [ ]
2026-01-20 18:13:12 +03:00
def chat_with_tools (
self ,
messages : list [ dict [ str , Any ] ] ,
tools : Optional [ list [ dict [ str , Any ] ] ] = None ,
tool_choice : Optional [ str ] = " auto " ,
) - > dict [ str , Any ] :
"""
Send chat messages to LLM with optional tool definitions .
This method handles conversation - style interactions with the LLM ,
including function calling / tool usage capabilities .
Args :
messages : List of message dictionaries . Each message should have :
- ' role ' : str - One of ' user ' , ' assistant ' , ' system ' , or ' tool '
- ' content ' : str - The message content
- ' tool_call_id ' : Optional [ str ] - For tool responses , the ID of the tool call
- ' name ' : Optional [ str ] - For tool messages , the tool name
tools : Optional list of tool definitions in OpenAI - compatible format .
Each tool should have ' type ' : ' function ' and ' function ' with :
- ' name ' : str - Tool name
- ' description ' : str - Tool description
- ' parameters ' : dict - JSON schema for parameters
tool_choice : How the model should handle tools :
- ' auto ' : Model decides whether to call tools
- ' none ' : Model must not call tools
- ' required ' : Model must call at least one tool
- Or a dict specifying a specific tool to call
* * kwargs : Additional provider - specific parameters .
Returns :
Dictionary with :
- ' content ' : Optional [ str ] - The text response from the LLM , None if tool calls
- ' tool_calls ' : Optional [ List [ Dict ] ] - List of tool calls if LLM wants to call tools .
Each tool call dict has :
- ' id ' : str - Unique identifier for this tool call
- ' name ' : str - Tool name to call
- ' arguments ' : dict - Arguments for the tool call ( parsed JSON )
- ' finish_reason ' : str - Reason generation stopped :
- ' stop ' : Normal completion
- ' tool_calls ' : LLM wants to call tools
- ' length ' : Hit token limit
- ' error ' : An error occurred
Raises :
NotImplementedError : If the provider doesn ' t implement this method.
"""
# Base implementation - each provider should override this
logger . warning (
f " { self . __class__ . __name__ } does not support chat_with_tools. "
" This method should be overridden by the provider implementation. "
)
return {
" content " : None ,
" tool_calls " : None ,
" finish_reason " : " error " ,
}
2024-06-22 00:30:19 +03:00
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 )