Skip to content

Commit

Permalink
Merge branch 'dev' of github.com:blakeblackshear/frigate into testing
Browse files Browse the repository at this point in the history
  • Loading branch information
ivanjx committed Sep 11, 2024
2 parents 46c4061 + e016bd6 commit 081d26c
Show file tree
Hide file tree
Showing 29 changed files with 1,153 additions and 271 deletions.
3 changes: 2 additions & 1 deletion docker/main/requirements-wheels.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ ws4py == 0.5.*
unidecode == 1.3.*
# OpenVino & ONNX
openvino == 2024.1.*
onnxruntime-openvino == 1.18.*
onnxruntime-openvino == 1.18.* ; platform_machine == 'x86_64'
onnxruntime == 1.18.* ; platform_machine == 'aarch64'
# Embeddings
onnx_clip == 4.0.*
chromadb == 0.5.0
Expand Down
6 changes: 3 additions & 3 deletions docs/docs/configuration/genai.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ id: genai
title: Generative AI
---

Generative AI can be used to automatically generate descriptions based on the thumbnails of your events. This helps with [semantic search](/configuration/semantic_search) in Frigate by providing detailed text descriptions as a basis of the search query.
Generative AI can be used to automatically generate descriptions based on the thumbnails of your tracked objects. This helps with [Semantic Search](/configuration/semantic_search) in Frigate by providing detailed text descriptions as a basis of the search query.

## Configuration

Expand Down Expand Up @@ -100,15 +100,15 @@ genai:

## Custom Prompts

Frigate sends multiple frames from the detection along with a prompt to your Generative AI provider asking it to generate a description. The default prompt is as follows:
Frigate sends multiple frames from the tracked object along with a prompt to your Generative AI provider asking it to generate a description. The default prompt is as follows:

```
Describe the {label} in the sequence of images with as much detail as possible. Do not describe the background.
```

:::tip

Prompts can use variable replacements like `{label}`, `{sub_label}`, and `{camera}` to substitute information from the detection as part of the prompt.
Prompts can use variable replacements like `{label}`, `{sub_label}`, and `{camera}` to substitute information from the tracked object as part of the prompt.

:::

Expand Down
18 changes: 11 additions & 7 deletions docs/docs/configuration/semantic_search.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ id: semantic_search
title: Using Semantic Search
---

Semantic search works by embedding images and/or text into a vector representation identified by numbers. Frigate has support for two such models which both run locally: [OpenAI CLIP](https://openai.com/research/clip) and [all-MiniLM-L6-v2](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2). Embeddings are then saved to a local instance of [ChromaDB](https://trychroma.com).
The Search feature in Frigate allows you to find tracked objects within your review items using either the image itself, a user-defined text description, or an automatically generated one. This semantic search functionality works by creating _embeddings_ — numerical vector representations — for both the images and text descriptions of your tracked objects. By comparing these embeddings, Frigate assesses their similarities to deliver relevant search results.

Frigate has support for two models to create embeddings, both of which run locally: [OpenAI CLIP](https://openai.com/research/clip) and [all-MiniLM-L6-v2](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2). Embeddings are then saved to a local instance of [ChromaDB](https://trychroma.com).

## Configuration

Expand All @@ -17,22 +19,24 @@ semantic_search:
:::tip
The embeddings database can be re-indexed from the existing detections in your database by adding `reindex: True` to your `semantic_search` configuration. Depending on the number of detections you have, it can take up to 30 minutes to complete and may max out your CPU while indexing. Make sure to set the config back to `False` before restarting Frigate again.
The embeddings database can be re-indexed from the existing tracked objects in your database by adding `reindex: True` to your `semantic_search` configuration. Depending on the number of tracked objects you have, it can take a long while to complete and may max out your CPU while indexing. Make sure to set the config back to `False` before restarting Frigate again.

If you are enabling the Search feature for the first time, be advised that Frigate does not automatically index older tracked objects. You will need to enable the `reindex` feature in order to do that.

:::

### OpenAI CLIP

This model is able to embed both images and text into the same vector space, which allows `image -> image` and `text -> image` similarity searches. Frigate uses this model on detections to encode the thumbnail image and store it in Chroma. When searching detections via text in the search box, frigate will perform a `text -> image` similarity search against this embedding. When clicking "FIND SIMILAR" next to a detection, Frigate will perform an `image -> image` similarity search to retrieve the closest matching thumbnails.
This model is able to embed both images and text into the same vector space, which allows `image -> image` and `text -> image` similarity searches. Frigate uses this model on tracked objects to encode the thumbnail image and store it in Chroma. When searching for tracked objects via text in the search box, Frigate will perform a `text -> image` similarity search against this embedding. When clicking "Find Similar" in the tracked object detail pane, Frigate will perform an `image -> image` similarity search to retrieve the closest matching thumbnails.

### all-MiniLM-L6-v2

This is a sentence embedding model that has been fine tuned on over 1 billion sentence pairs. This model is used to embed detection descriptions and perform searches against them. Descriptions can be created and/or modified on the search page when clicking on the info icon next to a detection. See [the Generative AI docs](/configuration/genai.md) for more information on how to automatically generate event descriptions.
This is a sentence embedding model that has been fine tuned on over 1 billion sentence pairs. This model is used to embed tracked object descriptions and perform searches against them. Descriptions can be created, viewed, and modified on the Search page when clicking on the gray tracked object chip at the top left of each review item. See [the Generative AI docs](/configuration/genai.md) for more information on how to automatically generate event descriptions.

## Usage Tips
## Usage

1. Semantic search is used in conjunction with the other filters available on the search page. Use a combination of traditional filtering and semantic search for the best results.
1. Semantic search is used in conjunction with the other filters available on the Search page. Use a combination of traditional filtering and semantic search for the best results.
2. The comparison between text and image embedding distances generally means that results matching `description` will appear first, even if a `thumbnail` embedding may be a better match. Play with the "Search Type" filter to help find what you are looking for.
3. Make your search language and tone closely match your descriptions. If you are using thumbnail search, phrase your query as an image caption.
4. Semantic search on thumbnails tends to return better results when matching large subjects that take up most of the frame. Small things like "cat" tend to not work well.
5. Experiment! Find a detection you want to test and start typing keywords to see what works for you.
5. Experiment! Find a tracked object you want to test and start typing keywords to see what works for you.
8 changes: 8 additions & 0 deletions docs/docs/integrations/mqtt.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,14 @@ Message published for each changed review item. The first message is published w

Same data available at `/api/stats` published at a configurable interval.

### `frigate/notifications/set`

Topic to turn notifications on and off. Expected values are `ON` and `OFF`.

### `frigate/notifications/state`

Topic with current state of notifications. Published values are `ON` and `OFF`.

## Frigate Camera Topics

### `frigate/<camera_name>/<object_name>`
Expand Down
50 changes: 30 additions & 20 deletions frigate/api/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ def event_ids():
@EventBp.route("/events/search")
def events_search():
query = request.args.get("query", type=str)
search_type = request.args.get("search_type", "text", type=str)
search_type = request.args.get("search_type", "thumbnail,description", type=str)
include_thumbnails = request.args.get("include_thumbnails", default=1, type=int)
limit = request.args.get("limit", 50, type=int)

Expand Down Expand Up @@ -358,7 +358,7 @@ def events_search():
thumb_ids = {}
desc_ids = {}

if search_type == "thumbnail":
if search_type == "similarity":
# Grab the ids of events that match the thumbnail image embeddings
try:
search_event: Event = Event.get(Event.id == query)
Expand All @@ -379,31 +379,41 @@ def events_search():
n_results=limit,
where=where,
)
thumb_ids = dict(zip(thumb_result["ids"][0], thumb_result["distances"][0]))
else:
thumb_result = context.embeddings.thumbnail.query(
query_texts=[query],
n_results=limit,
where=where,
)
# Do a rudimentary normalization of the difference in distances returned by CLIP and MiniLM.
thumb_ids = dict(
zip(
thumb_result["ids"][0],
context.thumb_stats.normalize(thumb_result["distances"][0]),
)
)
desc_result = context.embeddings.description.query(
query_texts=[query],
n_results=limit,
where=where,
)
desc_ids = dict(
zip(
desc_result["ids"][0],
context.desc_stats.normalize(desc_result["distances"][0]),
else:
search_types = search_type.split(",")

if "thumbnail" in search_types:
thumb_result = context.embeddings.thumbnail.query(
query_texts=[query],
n_results=limit,
where=where,
)
# Do a rudimentary normalization of the difference in distances returned by CLIP and MiniLM.
thumb_ids = dict(
zip(
thumb_result["ids"][0],
context.thumb_stats.normalize(thumb_result["distances"][0]),
)
)

if "description" in search_types:
desc_result = context.embeddings.description.query(
query_texts=[query],
n_results=limit,
where=where,
)
desc_ids = dict(
zip(
desc_result["ids"][0],
context.desc_stats.normalize(desc_result["distances"][0]),
)
)
)

results = {}
for event_id in thumb_ids.keys() | desc_ids:
Expand Down
3 changes: 2 additions & 1 deletion frigate/api/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,8 @@ def vod_ts(camera_name, start_ts, end_ts):
)


@MediaBp.route("/vod/<year_month>/<day>/<hour>/<camera_name>")
@MediaBp.route("/vod/<year_month>/<int:day>/<int:hour>/<camera_name>")
@MediaBp.route("/vod/<year_month>/<float:day>/<float:hour>/<camera_name>")
def vod_hour_no_timezone(year_month, day, hour, camera_name):
return vod_hour(
year_month, day, hour, camera_name, get_localzone_name().replace("/", ",")
Expand Down
5 changes: 4 additions & 1 deletion frigate/api/preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,10 @@ def preview_ts(camera_name, start_ts, end_ts):
return make_response(jsonify(clips), 200)


@PreviewBp.route("/preview/<year_month>/<day>/<hour>/<camera_name>/<tz_name>")
@PreviewBp.route("/preview/<year_month>/<int:day>/<int:hour>/<camera_name>/<tz_name>")
@PreviewBp.route(
"/preview/<year_month>/<float:day>/<float:hour>/<camera_name>/<tz_name>"
)
def preview_hour(year_month, day, hour, camera_name, tz_name):
parts = year_month.split("-")
start_date = (
Expand Down
2 changes: 1 addition & 1 deletion frigate/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ def init_dispatcher(self) -> None:
if self.config.mqtt.enabled:
comms.append(MqttClient(self.config))

if self.config.notifications.enabled:
if self.config.notifications.enabled_in_config:
comms.append(WebPushClient(self.config))

comms.append(WebSocketClient(self.config))
Expand Down
25 changes: 22 additions & 3 deletions frigate/comms/dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ def __init__(
"birdseye": self._on_birdseye_command,
"birdseye_mode": self._on_birdseye_mode_command,
}
self._global_settings_handlers: dict[str, Callable] = {
"notifications": self._on_notification_command,
}

for comm in self.comms:
comm.subscribe(self._receive)
Expand All @@ -86,9 +89,13 @@ def _receive(self, topic: str, payload: str) -> Optional[Any]:
if topic.endswith("set"):
try:
# example /cam_name/detect/set payload=ON|OFF
camera_name = topic.split("/")[-3]
command = topic.split("/")[-2]
self._camera_settings_handlers[command](camera_name, payload)
if topic.count("/") == 2:
camera_name = topic.split("/")[-3]
command = topic.split("/")[-2]
self._camera_settings_handlers[command](camera_name, payload)
elif topic.count("/") == 1:
command = topic.split("/")[-2]
self._global_settings_handlers[command](payload)
except IndexError:
logger.error(f"Received invalid set command: {topic}")
return
Expand Down Expand Up @@ -282,6 +289,18 @@ def _on_motion_threshold_command(self, camera_name: str, payload: int) -> None:
self.config_updater.publish(f"config/motion/{camera_name}", motion_settings)
self.publish(f"{camera_name}/motion_threshold/state", payload, retain=True)

def _on_notification_command(self, payload: str) -> None:
"""Callback for notification topic."""
if payload != "ON" and payload != "OFF":
f"Received unsupported value for notification: {payload}"
return

notification_settings = self.config.notifications
logger.info(f"Setting notifications: {payload}")
notification_settings.enabled = payload == "ON" # type: ignore[union-attr]
self.config_updater.publish("config/notifications", notification_settings)
self.publish("notifications/state", payload, retain=True)

def _on_audio_command(self, camera_name: str, payload: str) -> None:
"""Callback for audio topic."""
audio_settings = self.config.cameras[camera_name].audio
Expand Down
13 changes: 13 additions & 0 deletions frigate/comms/mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@ def _set_initial_topics(self) -> None:
retain=True,
)

if self.config.notifications.enabled_in_config:
self.publish(
"notifications/state",
"ON" if self.config.notifications.enabled else "OFF",
retain=True,
)

self.publish("available", "online", retain=True)

def on_mqtt_command(
Expand Down Expand Up @@ -209,6 +216,12 @@ def _start(self) -> None:
self.on_mqtt_command,
)

if self.config.notifications.enabled_in_config:
self.client.message_callback_add(
f"{self.mqtt_config.topic_prefix}/notifications/set",
self.on_mqtt_command,
)

self.client.message_callback_add(
f"{self.mqtt_config.topic_prefix}/restart", self.on_mqtt_command
)
Expand Down
13 changes: 13 additions & 0 deletions frigate/comms/webpush.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from py_vapid import Vapid01
from pywebpush import WebPusher

from frigate.comms.config_updater import ConfigSubscriber
from frigate.comms.dispatcher import Communicator
from frigate.config import FrigateConfig
from frigate.const import CONFIG_DIR
Expand Down Expand Up @@ -41,6 +42,9 @@ def __init__(self, config: FrigateConfig) -> None:
for sub in user["notification_tokens"]:
self.web_pushers[user["username"]].append(WebPusher(sub))

# notification config updater
self.config_subscriber = ConfigSubscriber("config/notifications")

def subscribe(self, receiver: Callable) -> None:
"""Wrapper for allowing dispatcher to subscribe."""
pass
Expand Down Expand Up @@ -101,6 +105,15 @@ def cleanup_registrations(self) -> None:

def publish(self, topic: str, payload: Any, retain: bool = False) -> None:
"""Wrapper for publishing when client is in valid state."""
# check for updated notification config
_, updated_notif_config = self.config_subscriber.check_for_update()

if updated_notif_config:
self.config.notifications = updated_notif_config

if not self.config.notifications.enabled:
return

if topic == "reviews":
self.send_alert(json.loads(payload))

Expand Down
6 changes: 6 additions & 0 deletions frigate/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ class AuthConfig(FrigateBaseModel):
class NotificationConfig(FrigateBaseModel):
enabled: bool = Field(default=False, title="Enable notifications")
email: Optional[str] = Field(default=None, title="Email required for push.")
enabled_in_config: Optional[bool] = Field(
default=None, title="Keep track of original state of notifications."
)


class StatsConfig(FrigateBaseModel):
Expand Down Expand Up @@ -1459,6 +1462,9 @@ def runtime_config(self, plus_api: PlusApi = None) -> FrigateConfig:
config.mqtt.user = config.mqtt.user.format(**FRIGATE_ENV_VARS)
config.mqtt.password = config.mqtt.password.format(**FRIGATE_ENV_VARS)

# set notifications state
config.notifications.enabled_in_config = config.notifications.enabled

# GenAI substitution
if config.genai.api_key:
config.genai.api_key = config.genai.api_key.format(**FRIGATE_ENV_VARS)
Expand Down
3 changes: 2 additions & 1 deletion frigate/object_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,7 @@ def update(self, frame_time, current_detections, motion_boxes, regions):
or obj.thumbnail_data["frame_time"] != frame_time
):
continue


if object_type in self.best_objects:
current_best = self.best_objects[object_type]
Expand Down Expand Up @@ -881,7 +882,7 @@ def update(self, frame_time, current_detections, motion_boxes, regions):
current_thumb_frames = {
obj.thumbnail_data["frame_time"]
for obj in tracked_objects.values()
if obj.thumbnail_data is not None and "frame_time" in obj.thumbnail_data and not obj.false_positive
if not obj.false_positive and obj.thumbnail_data is not None
}

current_best_frames = {
Expand Down
Loading

0 comments on commit 081d26c

Please sign in to comment.