mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-25 21:18:57 +03:00
CI / AMD64 Build (push) Canceled after 0s
CI / ARM Build (push) Canceled after 0s
CI / Jetson Jetpack 6 (push) Canceled after 0s
CI / AMD64 Extra Build (push) Canceled after 0s
CI / ARM Extra Build (push) Canceled after 0s
CI / Synaptics Build (push) Canceled after 0s
CI / Assemble and push default build (push) Canceled after 0s
* create GenAI post processors when a camera enables GenAI at runtime * fix types
214 lines
7.7 KiB
Python
214 lines
7.7 KiB
Python
"""Tests for GenAI enablement gating in the embeddings maintainer.
|
|
|
|
Covers creating post processors when GenAI is enabled at runtime, and the
|
|
per-camera gating those processors apply once they exist.
|
|
"""
|
|
|
|
import sys
|
|
import unittest
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
# Mock TFLite before importing the maintainer
|
|
_MOCK_MODULES = [
|
|
"tflite_runtime",
|
|
"tflite_runtime.interpreter",
|
|
"ai_edge_litert",
|
|
"ai_edge_litert.interpreter",
|
|
]
|
|
for mod in _MOCK_MODULES:
|
|
if mod not in sys.modules:
|
|
sys.modules[mod] = MagicMock()
|
|
|
|
# imported from the maintainer to avoid tripping the circular import between
|
|
# the maintainer and the processor modules
|
|
from frigate.embeddings.maintainer import ( # noqa: E402
|
|
EmbeddingMaintainer,
|
|
ObjectDescriptionProcessor,
|
|
PostProcessDataEnum,
|
|
ReviewDescriptionProcessor,
|
|
)
|
|
|
|
|
|
class TestGenAIProcessorSync(unittest.TestCase):
|
|
"""Enabling GenAI on the first camera must not require a restart."""
|
|
|
|
def _make_maintainer(
|
|
self,
|
|
review: bool = False,
|
|
objects: bool = False,
|
|
review_in_config: bool | None = None,
|
|
objects_in_config: bool | None = None,
|
|
) -> EmbeddingMaintainer:
|
|
# Bypass the heavy __init__; only the attributes touched by
|
|
# _sync_genai_processors are needed for these tests.
|
|
maintainer = EmbeddingMaintainer.__new__(EmbeddingMaintainer)
|
|
maintainer.post_processors = []
|
|
maintainer.config = MagicMock()
|
|
maintainer.config.cameras = {
|
|
"front": self._make_camera(
|
|
review,
|
|
objects,
|
|
review if review_in_config is None else review_in_config,
|
|
objects if objects_in_config is None else objects_in_config,
|
|
)
|
|
}
|
|
maintainer.config_updater = MagicMock()
|
|
maintainer.embeddings = None
|
|
maintainer.requestor = MagicMock()
|
|
maintainer.metrics = MagicMock()
|
|
maintainer.genai_manager = MagicMock()
|
|
maintainer.semantic_trigger_processor = None
|
|
return maintainer
|
|
|
|
def _make_camera(
|
|
self,
|
|
review: bool,
|
|
objects: bool,
|
|
review_in_config: bool,
|
|
objects_in_config: bool,
|
|
) -> MagicMock:
|
|
camera = MagicMock()
|
|
camera.review.genai.enabled = review
|
|
camera.review.genai.enabled_in_config = review_in_config
|
|
camera.objects.genai.enabled = objects
|
|
camera.objects.genai.enabled_in_config = objects_in_config
|
|
return camera
|
|
|
|
def _processor_types(self, maintainer: EmbeddingMaintainer) -> list[type]:
|
|
return [type(p) for p in maintainer.post_processors]
|
|
|
|
def test_no_processors_when_genai_disabled(self):
|
|
"""A config with no GenAI cameras registers neither processor."""
|
|
maintainer = self._make_maintainer()
|
|
|
|
maintainer._sync_genai_processors()
|
|
|
|
self.assertEqual(maintainer.post_processors, [])
|
|
|
|
def test_review_processor_added_when_enabled_after_startup(self):
|
|
"""Enabling review GenAI on the first camera registers the processor."""
|
|
maintainer = self._make_maintainer()
|
|
maintainer._sync_genai_processors()
|
|
|
|
camera = maintainer.config.cameras["front"]
|
|
camera.review.genai.enabled = True
|
|
camera.review.genai.enabled_in_config = True
|
|
maintainer._sync_genai_processors()
|
|
|
|
self.assertEqual(
|
|
self._processor_types(maintainer), [ReviewDescriptionProcessor]
|
|
)
|
|
|
|
def test_object_processor_added_when_enabled_after_startup(self):
|
|
"""Enabling object GenAI on the first camera registers the processor."""
|
|
maintainer = self._make_maintainer()
|
|
maintainer._sync_genai_processors()
|
|
|
|
camera = maintainer.config.cameras["front"]
|
|
camera.objects.genai.enabled = True
|
|
camera.objects.genai.enabled_in_config = True
|
|
maintainer._sync_genai_processors()
|
|
|
|
self.assertEqual(
|
|
self._processor_types(maintainer), [ObjectDescriptionProcessor]
|
|
)
|
|
|
|
def test_processor_added_when_only_enabled_by_profile(self):
|
|
"""A profile enables GenAI without setting enabled_in_config."""
|
|
maintainer = self._make_maintainer(
|
|
review=True, objects=True, review_in_config=False, objects_in_config=False
|
|
)
|
|
|
|
maintainer._sync_genai_processors()
|
|
|
|
self.assertEqual(
|
|
self._processor_types(maintainer),
|
|
[ReviewDescriptionProcessor, ObjectDescriptionProcessor],
|
|
)
|
|
|
|
def test_processors_are_not_duplicated(self):
|
|
"""Repeated config updates must not register a second processor."""
|
|
maintainer = self._make_maintainer(review=True, objects=True)
|
|
|
|
maintainer._sync_genai_processors()
|
|
maintainer._sync_genai_processors()
|
|
|
|
self.assertEqual(
|
|
self._processor_types(maintainer),
|
|
[ReviewDescriptionProcessor, ObjectDescriptionProcessor],
|
|
)
|
|
|
|
def test_genai_topic_triggers_sync(self):
|
|
"""A camera config update on a GenAI topic registers the processor."""
|
|
maintainer = self._make_maintainer(review=True)
|
|
maintainer.config_updater.check_for_updates.return_value = {"review": ["front"]}
|
|
|
|
maintainer._check_camera_config_updates()
|
|
|
|
self.assertEqual(
|
|
self._processor_types(maintainer), [ReviewDescriptionProcessor]
|
|
)
|
|
|
|
def test_unrelated_topic_does_not_sync(self):
|
|
"""An unrelated camera config update must not register processors."""
|
|
maintainer = self._make_maintainer(review=True)
|
|
maintainer.config_updater.check_for_updates.return_value = {"motion": ["front"]}
|
|
|
|
maintainer._check_camera_config_updates()
|
|
|
|
self.assertEqual(maintainer.post_processors, [])
|
|
|
|
|
|
class TestObjectDescriptionCameraGating(unittest.TestCase):
|
|
"""One camera enabling object descriptions must not enlist the others."""
|
|
|
|
def _make_processor(self, enabled: bool) -> ObjectDescriptionProcessor:
|
|
config = MagicMock()
|
|
camera = MagicMock()
|
|
camera.objects.genai.enabled = enabled
|
|
camera.objects.genai.send_triggers.after_significant_updates = None
|
|
config.cameras = {"front": camera}
|
|
|
|
genai_manager = MagicMock()
|
|
genai_manager.description_client = MagicMock()
|
|
|
|
return ObjectDescriptionProcessor(
|
|
config, None, MagicMock(), MagicMock(), genai_manager, None
|
|
)
|
|
|
|
def _update(self, processor: ObjectDescriptionProcessor) -> None:
|
|
processor.process_data(
|
|
{
|
|
"camera": "front",
|
|
"data": {
|
|
"id": "1234.5-abcdef",
|
|
"box": (0, 0, 10, 10),
|
|
"stationary": False,
|
|
},
|
|
"state": "update",
|
|
"yuv_frame": MagicMock(),
|
|
},
|
|
PostProcessDataEnum.tracked_object,
|
|
)
|
|
|
|
@patch("frigate.data_processing.post.object_descriptions.create_thumbnail")
|
|
def test_disabled_camera_collects_no_thumbnails(self, mock_create_thumbnail):
|
|
"""A camera with object descriptions off does no thumbnail work."""
|
|
processor = self._make_processor(enabled=False)
|
|
|
|
self._update(processor)
|
|
|
|
mock_create_thumbnail.assert_not_called()
|
|
self.assertEqual(processor.tracked_events, {})
|
|
|
|
@patch("frigate.data_processing.post.object_descriptions.create_thumbnail")
|
|
def test_enabled_camera_collects_thumbnails(self, mock_create_thumbnail):
|
|
"""A camera with object descriptions on still collects thumbnails."""
|
|
mock_create_thumbnail.return_value = b"jpg"
|
|
processor = self._make_processor(enabled=True)
|
|
|
|
self._update(processor)
|
|
|
|
mock_create_thumbnail.assert_called_once()
|
|
self.assertEqual(len(processor.tracked_events["1234.5-abcdef"]), 1)
|