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
2026-04-27 01:09:35 +03:00
import json
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
2026-03-25 18:28:48 +03:00
from typing import Any , Callable , 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-04-27 01:09:35 +03:00
from pydantic import ValidationError
2024-10-14 15:23:10 +03:00
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 = { }
2026-03-25 18:28:48 +03:00
def register_genai_provider ( key : GenAIProviderEnum ) - > Callable :
2024-06-22 00:30:19 +03:00
""" Register a GenAI provider. """
2026-03-25 18:28:48 +03:00
def decorator ( cls : type ) - > type :
2024-06-22 00:30:19 +03:00
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
2026-03-10 03:47:37 +03:00
Describe the scene based on observable actions and movements , evaluate the activity against the Activity Indicators above , and assign a potential_threat_level ( 0 , 1 , or 2 ) by applying the threat level indicators consistently .
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
2026-03-10 03:47:37 +03:00
## Response Field Guidelines
2025-10-26 00:40:04 +03:00
2026-03-10 03:47:37 +03:00
Respond with a JSON object matching the provided schema . Field - specific guidance :
2026-04-07 16:16:19 +03:00
- ` scene ` : Describe how the sequence begins , then the progression of events — all significant movements and actions in order . For example , if a vehicle arrives and then a person exits , describe both sequentially . For named subjects ( those with a ` ← ` separator in " Objects in Scene " ) , always use their name — do not replace them with generic terms . For unnamed objects ( e . g . , " person " , " car " ) , refer to them naturally with articles ( e . g . , " a person " , " the car " ) . Your description should align with and support the threat level you assign .
- ` title ` : Characterize * * what took place and where * * — interpret the overall purpose or outcome , do not simply compress the scene description into fewer words . Include the relevant location ( zone , area , or entry point ) . For named subjects , always use their name . For unnamed objects , refer to them naturally with articles . No editorial qualifiers like " routine " or " suspicious. "
2026-03-10 03:47:37 +03:00
- ` potential_threat_level ` : Must be consistent with your scene description and the activity patterns 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
2026-03-19 19:39:24 +03:00
Each line represents a detection state , not necessarily unique individuals . The ` ← ` symbol separates a recognized subject ' s name from their object type — use only the name (before the `←`) in your response, not the type after it. The same subject may appear across multiple lines if detected multiple times.
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
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-03-10 03:47:37 +03:00
# Build JSON schema for structured output from ReviewMetadata model
schema = ReviewMetadata . model_json_schema ( )
schema . get ( " properties " , { } ) . pop ( " time " , None )
if " time " in schema . get ( " required " , [ ] ) :
schema [ " required " ] . remove ( " time " )
if not concerns :
schema . get ( " properties " , { } ) . pop ( " other_concerns " , None )
if " other_concerns " in schema . get ( " required " , [ ] ) :
schema [ " required " ] . remove ( " other_concerns " )
response_format = {
" type " : " json_schema " ,
" json_schema " : {
" name " : " review_metadata " ,
" strict " : True ,
" schema " : schema ,
} ,
}
response = self . _send ( context_prompt , thumbnails , response_format )
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 )
2026-04-27 01:09:35 +03:00
except ValidationError as ve :
# Constraint violations (length, item count, ranges) are logged
# at debug and the response is kept anyway — a slightly
# off-spec answer is still usable, and dropping the whole
# response loses the narrative content the model produced.
for err in ve . errors ( ) :
loc = " . " . join ( str ( p ) for p in err [ " loc " ] ) or " <root> "
logger . debug (
" Review metadata soft validation: %s — %s (input: %r ) " ,
loc ,
err [ " msg " ] ,
err . get ( " input " ) ,
)
try :
raw = json . loads ( clean_json )
except json . JSONDecodeError as je :
logger . error ( " Failed to parse review description JSON: %s " , je )
return None
2026-04-28 17:54:09 +03:00
# observations and confidence are required on the model; fill an empty default
2026-04-27 01:09:35 +03:00
# if the response omitted it so attribute access stays safe.
raw . setdefault ( " observations " , [ ] )
2026-04-28 17:54:09 +03:00
raw . setdefault ( " confidence " , 0.0 )
2026-04-27 01:09:35 +03:00
metadata = ReviewMetadata . model_construct ( * * raw )
except Exception as e :
logger . error (
f " Failed to parse review description as the response did not match expected format. { e } "
)
return None
2025-08-15 16:25:49 +03:00
2026-04-27 01:09:35 +03:00
try :
2026-03-10 16:35:00 +03:00
# Normalize confidence if model returned a percentage (e.g. 85 instead of 0.85)
if metadata . confidence > 1.0 :
metadata . confidence = min ( metadata . confidence / 100.0 , 1.0 )
2026-03-19 19:39:24 +03:00
# If any verified objects (contain ← separator), 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
2026-04-07 16:16:19 +03:00
metadata . title = metadata . title [ 0 ] . upper ( ) + metadata . title [ 1 : ]
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 :
2026-04-27 01:09:35 +03:00
logger . error ( f " Failed to post-process review metadata: { e } " )
2025-08-10 14:57:54 +03:00
return None
else :
2026-04-05 19:08:23 +03:00
logger . debug (
f " Invalid response received from GenAI provider for review description on { review_data [ ' camera ' ] } . Response: { response } " ,
)
2025-08-10 14:57:54 +03:00
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 (
2026-03-25 18:28:48 +03:00
str ( 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 )
2026-03-25 18:28:48 +03:00
def _init_provider ( self ) - > Any :
2024-06-22 00:30:19 +03:00
""" Initialize the client. """
return None
2026-03-10 03:47:37 +03:00
def _send (
self ,
prompt : str ,
images : list [ bytes ] ,
response_format : Optional [ dict ] = None ,
) - > Optional [ str ] :
2026-01-29 21:30:21 +03:00
""" Submit a request to the provider. """
2024-06-22 00:30:19 +03:00
return None
2026-04-04 02:13:52 +03:00
@property
def supports_vision ( self ) - > bool :
""" Whether the model supports vision/image input.
Defaults to True for cloud providers . Providers that can detect
capability at runtime ( e . g . llama . cpp ) should override this .
"""
return True
def list_models ( self ) - > list [ str ] :
""" Return the list of model names available from this provider.
Providers should override this to query their backend .
"""
return [ ]
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-04-26 01:38:18 +03:00
def estimate_image_tokens ( self , width : int , height : int ) - > float :
""" Estimate prompt tokens consumed by a single image of the given dimensions.
Default heuristic : ~ 1 token per 1250 pixels . Providers that can measure or
know their model ' s exact image-token cost should override.
"""
return ( width * height ) / 1250
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
2026-03-25 18:28:48 +03:00
def load_providers ( ) - > None :
2024-06-22 00:30:19 +03:00
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 )