mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-27 17:17:40 +03:00
use thumbnail and description for trigger types
This commit is contained in:
parent
e759557a4f
commit
a17e202a9e
@ -48,6 +48,6 @@ class SubmitPlusBody(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class TriggerEmbeddingBody(BaseModel):
|
class TriggerEmbeddingBody(BaseModel):
|
||||||
type: Literal["text", "image", "both"]
|
type: Literal["thumbnail", "description"]
|
||||||
data: str
|
data: str
|
||||||
threshold: float = Field(default=0.5, ge=0.0, le=1.0)
|
threshold: float = Field(default=0.5, ge=0.0, le=1.0)
|
||||||
|
|||||||
@ -1479,9 +1479,9 @@ def create_trigger_embedding(
|
|||||||
context: EmbeddingsContext = request.app.embeddings
|
context: EmbeddingsContext = request.app.embeddings
|
||||||
# Generate embedding based on type
|
# Generate embedding based on type
|
||||||
embedding = None
|
embedding = None
|
||||||
if body.type == "text" or body.type == "both":
|
if body.type == "description":
|
||||||
embedding = context.generate_description_embedding(body.data)
|
embedding = context.generate_description_embedding(body.data)
|
||||||
elif body.type == "image":
|
elif body.type == "thumbnail":
|
||||||
try:
|
try:
|
||||||
event: Event = Event.get(Event.id == body.data)
|
event: Event = Event.get(Event.id == body.data)
|
||||||
except DoesNotExist:
|
except DoesNotExist:
|
||||||
@ -1584,9 +1584,9 @@ def update_trigger_embedding(
|
|||||||
context: EmbeddingsContext = request.app.embeddings
|
context: EmbeddingsContext = request.app.embeddings
|
||||||
# Generate embedding based on type
|
# Generate embedding based on type
|
||||||
embedding = None
|
embedding = None
|
||||||
if body.type == "text" or body.type == "both":
|
if body.type == "description":
|
||||||
embedding = context.generate_description_embedding(body.data)
|
embedding = context.generate_description_embedding(body.data)
|
||||||
elif body.type == "image":
|
elif body.type == "thumbnail":
|
||||||
try:
|
try:
|
||||||
event: Event = Event.get(Event.id == body.data)
|
event: Event = Event.get(Event.id == body.data)
|
||||||
except DoesNotExist:
|
except DoesNotExist:
|
||||||
|
|||||||
@ -26,9 +26,8 @@ class EnrichmentsDeviceEnum(str, Enum):
|
|||||||
|
|
||||||
|
|
||||||
class TriggerType(str, Enum):
|
class TriggerType(str, Enum):
|
||||||
TEXT = "text"
|
THUMBNAIL = "thumbnail"
|
||||||
IMAGE = "image"
|
DESCRIPTION = "description"
|
||||||
BOTH = "both"
|
|
||||||
|
|
||||||
|
|
||||||
class TriggerAction(str, Enum):
|
class TriggerAction(str, Enum):
|
||||||
@ -126,7 +125,7 @@ class SemanticSearchConfig(FrigateBaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class TriggerConfig(FrigateBaseModel):
|
class TriggerConfig(FrigateBaseModel):
|
||||||
type: TriggerType = Field(default=TriggerType.TEXT, title="Type of trigger")
|
type: TriggerType = Field(default=TriggerType.DESCRIPTION, title="Type of trigger")
|
||||||
data: str = Field(title="Trigger content (text phrase or image ID)")
|
data: str = Field(title="Trigger content (text phrase or image ID)")
|
||||||
threshold: float = Field(
|
threshold: float = Field(
|
||||||
title="Confidence score required to run the trigger.",
|
title="Confidence score required to run the trigger.",
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
"""Real time processor to trigger alerts by matching embeddings."""
|
"""Real time processor to trigger alerts by matching embeddings."""
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
@ -10,6 +12,7 @@ from peewee import DoesNotExist
|
|||||||
|
|
||||||
from frigate.comms.inter_process import InterProcessRequestor
|
from frigate.comms.inter_process import InterProcessRequestor
|
||||||
from frigate.config import FrigateConfig
|
from frigate.config import FrigateConfig
|
||||||
|
from frigate.const import CONFIG_DIR
|
||||||
from frigate.data_processing.types import PostProcessDataEnum
|
from frigate.data_processing.types import PostProcessDataEnum
|
||||||
from frigate.db.sqlitevecq import SqliteVecQueueDatabase
|
from frigate.db.sqlitevecq import SqliteVecQueueDatabase
|
||||||
from frigate.embeddings.util import ZScoreNormalization
|
from frigate.embeddings.util import ZScoreNormalization
|
||||||
@ -41,6 +44,16 @@ class SemanticTriggerProcessor(PostProcessorApi):
|
|||||||
self.trigger_embeddings: list[np.ndarray] = []
|
self.trigger_embeddings: list[np.ndarray] = []
|
||||||
|
|
||||||
self.thumb_stats = ZScoreNormalization()
|
self.thumb_stats = ZScoreNormalization()
|
||||||
|
self.desc_stats = ZScoreNormalization()
|
||||||
|
|
||||||
|
# load stats from disk
|
||||||
|
try:
|
||||||
|
with open(os.path.join(CONFIG_DIR, ".search_stats.json"), "r") as f:
|
||||||
|
data = json.loads(f.read())
|
||||||
|
self.thumb_stats.from_dict(data["thumb_stats"])
|
||||||
|
self.desc_stats.from_dict(data["desc_stats"])
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
def process_data(
|
def process_data(
|
||||||
self, data: dict[str, Any], data_type: PostProcessDataEnum
|
self, data: dict[str, Any], data_type: PostProcessDataEnum
|
||||||
@ -105,60 +118,22 @@ class SemanticTriggerProcessor(PostProcessorApi):
|
|||||||
trigger_embedding = np.frombuffer(trigger["embedding"], dtype=np.float32)
|
trigger_embedding = np.frombuffer(trigger["embedding"], dtype=np.float32)
|
||||||
|
|
||||||
# Determine which embedding to compare based on trigger type
|
# Determine which embedding to compare based on trigger type
|
||||||
if trigger["type"] == "image" and thumbnail_embedding is not None:
|
if trigger["type"] == "text" and thumbnail_embedding is not None:
|
||||||
data_embedding = thumbnail_embedding
|
data_embedding = thumbnail_embedding
|
||||||
normalized_distance = self.thumb_stats.normalize(
|
normalized_distance = self.thumb_stats.normalize(
|
||||||
[cosine_distance(data_embedding, trigger_embedding)],
|
[cosine_distance(data_embedding, trigger_embedding)],
|
||||||
save_stats=False,
|
save_stats=False,
|
||||||
)[0]
|
)[0]
|
||||||
elif trigger["type"] == "text" and description_embedding is not None:
|
elif trigger["type"] == "thumbnail" and thumbnail_embedding is not None:
|
||||||
data_embedding = description_embedding
|
data_embedding = thumbnail_embedding
|
||||||
normalized_distance = cosine_distance(data_embedding, trigger_embedding)
|
normalized_distance = cosine_distance(data_embedding, trigger_embedding)
|
||||||
elif trigger["type"] == "both":
|
elif trigger["type"] == "description" and description_embedding is not None:
|
||||||
# For "both" type triggers, check both embeddings and use the best match
|
data_embedding = description_embedding
|
||||||
similarities = []
|
normalized_distance = self.desc_stats.normalize(
|
||||||
similarity_sources = [] # Track which embedding produced each similarity
|
[cosine_distance(data_embedding, trigger_embedding)],
|
||||||
|
save_stats=False,
|
||||||
|
)[0]
|
||||||
|
|
||||||
if thumbnail_embedding is not None:
|
|
||||||
thumb_distance = cosine_distance(
|
|
||||||
thumbnail_embedding, trigger_embedding
|
|
||||||
)
|
|
||||||
thumb_normalized = self.thumb_stats.normalize(
|
|
||||||
[thumb_distance], save_stats=False
|
|
||||||
)[0]
|
|
||||||
thumb_similarity = 1 - thumb_normalized
|
|
||||||
similarities.append(thumb_similarity)
|
|
||||||
similarity_sources.append("thumbnail")
|
|
||||||
|
|
||||||
if description_embedding is not None:
|
|
||||||
desc_distance = cosine_distance(
|
|
||||||
description_embedding, trigger_embedding
|
|
||||||
)
|
|
||||||
desc_similarity = 1 - desc_distance
|
|
||||||
similarities.append(desc_similarity)
|
|
||||||
similarity_sources.append("description")
|
|
||||||
|
|
||||||
if not similarities:
|
|
||||||
continue # Skip if no valid embeddings
|
|
||||||
|
|
||||||
# Find the best similarity and its source
|
|
||||||
max_similarity_idx = similarities.index(max(similarities))
|
|
||||||
similarity = similarities[max_similarity_idx]
|
|
||||||
selected_source = similarity_sources[max_similarity_idx]
|
|
||||||
normalized_distance = 1 - similarity
|
|
||||||
|
|
||||||
# Debug log showing all similarities and which was selected
|
|
||||||
if len(similarities) > 1:
|
|
||||||
logger.debug(
|
|
||||||
f"Both embeddings available for trigger '{trigger['name']}': "
|
|
||||||
f"thumbnail={similarities[0]:.4f}, description={similarities[1]:.4f}, "
|
|
||||||
f"selected={selected_source} with similarity={similarity:.4f}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.debug(
|
|
||||||
f"Single embedding available for trigger '{trigger['name']}': "
|
|
||||||
f"{selected_source}={similarity:.4f}"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
# Skip trigger if embedding type doesn't match available data
|
# Skip trigger if embedding type doesn't match available data
|
||||||
continue
|
continue
|
||||||
@ -166,7 +141,7 @@ class SemanticTriggerProcessor(PostProcessorApi):
|
|||||||
similarity = 1 - normalized_distance
|
similarity = 1 - normalized_distance
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Trigger for {trigger['data'] if trigger['type'] == 'text' else 'image/both'} "
|
f"Trigger for {trigger['data'] if trigger['type'] == 'text' or trigger['type'] == 'description' else 'image'} "
|
||||||
f"(camera: {trigger['camera']}): normalized: {normalized_distance:.4f}, "
|
f"(camera: {trigger['camera']}): normalized: {normalized_distance:.4f}, "
|
||||||
f"similarity: {similarity:.4f}, threshold: {trigger['threshold']}"
|
f"similarity: {similarity:.4f}, threshold: {trigger['threshold']}"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -663,9 +663,8 @@
|
|||||||
"deleteTrigger": "Delete Trigger"
|
"deleteTrigger": "Delete Trigger"
|
||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"image": "Image",
|
"thumbnail": "Thumbnail",
|
||||||
"text": "Text",
|
"description": "Description"
|
||||||
"both": "Both"
|
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"alert": "Mark as Alert",
|
"alert": "Mark as Alert",
|
||||||
|
|||||||
@ -86,7 +86,7 @@ export default function CreateTriggerDialog({
|
|||||||
!existingTriggerNames.includes(value) || value === trigger?.name,
|
!existingTriggerNames.includes(value) || value === trigger?.name,
|
||||||
t("triggers.dialog.form.name.error.alreadyExists"),
|
t("triggers.dialog.form.name.error.alreadyExists"),
|
||||||
),
|
),
|
||||||
type: z.enum(["image", "text", "both"]),
|
type: z.enum(["thumbnail", "description"]),
|
||||||
data: z.string().min(1, t("triggers.dialog.form.content.error.required")),
|
data: z.string().min(1, t("triggers.dialog.form.content.error.required")),
|
||||||
threshold: z
|
threshold: z
|
||||||
.number()
|
.number()
|
||||||
@ -102,7 +102,7 @@ export default function CreateTriggerDialog({
|
|||||||
mode: "onChange",
|
mode: "onChange",
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: trigger?.name ?? "",
|
name: trigger?.name ?? "",
|
||||||
type: trigger?.type ?? "text",
|
type: trigger?.type ?? "description",
|
||||||
data: trigger?.data ?? "",
|
data: trigger?.data ?? "",
|
||||||
threshold: trigger?.threshold ?? 0.5,
|
threshold: trigger?.threshold ?? 0.5,
|
||||||
actions: trigger?.actions ?? ["alert"],
|
actions: trigger?.actions ?? ["alert"],
|
||||||
@ -129,7 +129,7 @@ export default function CreateTriggerDialog({
|
|||||||
if (!show) {
|
if (!show) {
|
||||||
form.reset({
|
form.reset({
|
||||||
name: "",
|
name: "",
|
||||||
type: "text",
|
type: "description",
|
||||||
data: "",
|
data: "",
|
||||||
threshold: 0.5,
|
threshold: 0.5,
|
||||||
actions: ["alert"],
|
actions: ["alert"],
|
||||||
@ -214,14 +214,11 @@ export default function CreateTriggerDialog({
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="image">
|
<SelectItem value="thumbnail">
|
||||||
{t("triggers.type.image")}
|
{t("triggers.type.thumbnail")}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="text">
|
<SelectItem value="description">
|
||||||
{t("triggers.type.text")}
|
{t("triggers.type.description")}
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="both">
|
|
||||||
{t("triggers.type.both")}
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@ -238,7 +235,7 @@ export default function CreateTriggerDialog({
|
|||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t("triggers.dialog.form.content.title")}
|
{t("triggers.dialog.form.content.title")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
{form.watch("type") === "image" ? (
|
{form.watch("type") === "thumbnail" ? (
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<ImagePicker
|
<ImagePicker
|
||||||
selectedImageId={field.value}
|
selectedImageId={field.value}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
export type TriggerType = "image" | "text" | "both";
|
export type TriggerType = "thumbnail" | "description";
|
||||||
export type TriggerAction = "alert" | "notification";
|
export type TriggerAction = "alert" | "notification";
|
||||||
|
|
||||||
export type Trigger = {
|
export type Trigger = {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user