From d3bc434e2dc2b14e3acde3236d3c55d94483533c Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 29 Oct 2026 16:16:35 -0500 Subject: [PATCH] adapt analytics to free-form model scenes, split notice counts, and peewee 4 --- docs/static/frigate-analytics-schema.json | 60 +++++++++++-------- frigate/analytics/collectors/detection.py | 21 ++++--- frigate/analytics/collectors/health.py | 9 ++- frigate/analytics/schema.py | 12 +++- frigate/models.py | 2 +- frigate/notices/registry.py | 3 +- .../test_analytics_collectors_detection.py | 25 +++++++- .../test_analytics_collectors_features.py | 4 +- .../test/test_analytics_collectors_health.py | 22 ++++--- frigate/test/test_analytics_report.py | 4 +- frigate/test/test_analytics_reporter.py | 6 +- frigate/test/test_notice_registry.py | 3 +- 12 files changed, 111 insertions(+), 60 deletions(-) diff --git a/docs/static/frigate-analytics-schema.json b/docs/static/frigate-analytics-schema.json index f578b159a3..b11b63409f 100644 --- a/docs/static/frigate-analytics-schema.json +++ b/docs/static/frigate-analytics-schema.json @@ -373,6 +373,7 @@ "intel-qsv", "jetson", "rpi", + "apple-silicon", "other" ], "title": "DecodeFamily", @@ -381,6 +382,12 @@ "DetectionModel": { "additionalProperties": false, "properties": { + "default": { + "description": "Whether the model serves cameras that set no scene", + "title": "Default", + "type": "boolean", + "x-public": true + }, "detector": { "$ref": "#/$defs/DetectorTypeEnum", "description": "Detector type the model runs on", @@ -396,7 +403,7 @@ "inference_ms": { "anyOf": [ { - "minimum": 0.0, + "minimum": 0, "type": "number" }, { @@ -426,6 +433,7 @@ } }, "required": [ + "default", "detector", "devices", "model_type", @@ -441,26 +449,23 @@ "properties": { "detection_fps": { "description": "Detections per second across cameras", - "minimum": 0.0, + "minimum": 0, "title": "Detection Fps", "type": "number", "x-public": true }, "models": { - "additionalProperties": { + "description": "Detection models", + "items": { "$ref": "#/$defs/DetectionModel" }, - "description": "Detection models keyed by scene", - "propertyNames": { - "$ref": "#/$defs/SceneEnum" - }, "title": "Models", - "type": "object", + "type": "array", "x-public": true }, "skipped_fps": { "description": "Frames per second skipped across cameras", - "minimum": 0.0, + "minimum": 0, "title": "Skipped Fps", "type": "number", "x-public": true @@ -476,6 +481,7 @@ }, "DetectorTypeEnum": { "enum": [ + "axelera", "axengine", "cpu", "deepx", @@ -772,6 +778,7 @@ "nvidia", "rockchip", "rpi", + "apple-silicon", "other" ], "title": "GpuVendor", @@ -785,12 +792,14 @@ "openvino:NPU", "onnx:amd", "onnx:nvidia", + "onnx:lighter", "tensorrt", "hailo", "memryx", "deepx", "rknn", "axengine", + "axelera", "synaptics", "cpu", "other" @@ -945,6 +954,8 @@ "enum": [ "rpi-64-h264", "rpi-64-h265", + "apple-silicon-h264", + "apple-silicon-h265", "vaapi", "intel-qsv-h264", "intel-qsv-h265", @@ -1091,10 +1102,17 @@ "NoticeCounts": { "additionalProperties": false, "properties": { - "dismissals": { - "description": "Dismissals since the last accepted report", + "acknowledgements": { + "description": "Acknowledgements since the last accepted report", "minimum": 0, - "title": "Dismissals", + "title": "Acknowledgements", + "type": "integer", + "x-public": true + }, + "mutes": { + "description": "Mutes since the last accepted report", + "minimum": 0, + "title": "Mutes", "type": "integer", "x-public": true }, @@ -1108,7 +1126,8 @@ }, "required": [ "occurrences", - "dismissals" + "acknowledgements", + "mutes" ], "title": "NoticeCounts", "type": "object" @@ -1118,7 +1137,10 @@ "detector_stuck", "model_download_failed", "skipped_detections", + "ffmpeg_high_cpu", + "detect_high_cpu", "shm_too_low", + "object_processing_behind", "failed_login" ], "title": "NoticeKindKey", @@ -1195,18 +1217,6 @@ "title": "RetainDays", "type": "object" }, - "SceneEnum": { - "description": "The camera environment a detection model is intended for.", - "enum": [ - "all", - "indoor", - "outdoor", - "indoor_thermal", - "outdoor_thermal" - ], - "title": "SceneEnum", - "type": "string" - }, "SemanticSearchModel": { "enum": [ "jinav1", diff --git a/frigate/analytics/collectors/detection.py b/frigate/analytics/collectors/detection.py index 1815b3e74d..d3939b80fd 100644 --- a/frigate/analytics/collectors/detection.py +++ b/frigate/analytics/collectors/detection.py @@ -7,7 +7,7 @@ from frigate.analytics.context import ReportContext from frigate.analytics.schema import DetectionModel, DetectionSection, ModelSource from frigate.config.config import DEFAULT_MODEL from frigate.const import MODEL_CACHE_DIR -from frigate.detectors.detector_config import SceneEnum +from frigate.detectors.detector_config import DEFAULT_SCENE from frigate.detectors.detector_types import DetectorTypeEnum from frigate.detectors.device import runner_names @@ -38,7 +38,7 @@ def collect(ctx: ReportContext) -> DetectionSection: # FrigateApp.start_detectors names the processes in this same order names = iter(runner_names([spec for _, specs in model_specs for spec in specs])) - models: dict[SceneEnum, DetectionModel] = {} + models: list[DetectionModel] = [] for model, specs in model_specs: speeds: list[float] = [] @@ -49,13 +49,16 @@ def collect(ctx: ReportContext) -> DetectionSection: if isinstance(speed, int | float) and speed > 0: speeds.append(float(speed)) - models[model.scene] = DetectionModel( - detector=DetectorTypeEnum(specs[0].detector), - devices=len(specs), - model_type=model.model_type, - input=f"{model.width}x{model.height}", - source=model_source(model.path), - inference_ms=rate(sum(speeds) / len(speeds)) if speeds else None, + models.append( + DetectionModel( + default=model.scene == DEFAULT_SCENE, + detector=DetectorTypeEnum(specs[0].detector), + devices=len(specs), + model_type=model.model_type, + input=f"{model.width}x{model.height}", + source=model_source(model.path), + inference_ms=rate(sum(speeds) / len(speeds)) if speeds else None, + ) ) return DetectionSection( diff --git a/frigate/analytics/collectors/health.py b/frigate/analytics/collectors/health.py index e40b525476..5fcf2b408c 100644 --- a/frigate/analytics/collectors/health.py +++ b/frigate/analytics/collectors/health.py @@ -32,11 +32,14 @@ def notice_deltas(notice_stats: list[dict[str, Any]]) -> dict[Any, NoticeCounts] continue occurrences = max(row["occurrences"] - row["reported_occurrences"], 0) - dismissals = max(row["dismissals"] - row["reported_dismissals"], 0) + acknowledgements = max( + row["acknowledgements"] - row["reported_acknowledgements"], 0 + ) + mutes = max(row["mutes"] - row["reported_mutes"], 0) - if occurrences or dismissals: + if occurrences or acknowledgements or mutes: deltas[NoticeKindKey(row["kind"])] = NoticeCounts( - occurrences=occurrences, dismissals=dismissals + occurrences=occurrences, acknowledgements=acknowledgements, mutes=mutes ) return deltas diff --git a/frigate/analytics/schema.py b/frigate/analytics/schema.py index 68710579d0..bf38b2cd1b 100644 --- a/frigate/analytics/schema.py +++ b/frigate/analytics/schema.py @@ -13,7 +13,7 @@ from frigate.config.camera.birdseye import BirdseyeModeEnum from frigate.config.camera.camera import CameraTypeEnum from frigate.config.camera.genai import GenAIProviderEnum, GenAIRoleEnum from frigate.config.classification import ModelSizeEnum -from frigate.detectors.detector_config import ModelTypeEnum, SceneEnum +from frigate.detectors.detector_config import ModelTypeEnum from frigate.detectors.detector_types import DetectorTypeEnum from frigate.ffmpeg_presets import PRESETS_HW_ACCEL_DECODE, PRESETS_INPUT from frigate.notices.types import NOTICE_KINDS @@ -53,6 +53,7 @@ class GpuVendor(StrEnum): nvidia = "nvidia" rockchip = "rockchip" rpi = "rpi" + apple_silicon = "apple-silicon" other = "other" @@ -63,6 +64,7 @@ class DecodeFamily(StrEnum): intel_qsv = "intel-qsv" jetson = "jetson" rpi = "rpi" + apple_silicon = "apple-silicon" other = "other" @@ -73,12 +75,14 @@ class HardwareKey(StrEnum): openvino_npu = "openvino:NPU" onnx_amd = "onnx:amd" onnx_nvidia = "onnx:nvidia" + onnx_lighter = "onnx:lighter" tensorrt = "tensorrt" hailo = "hailo" memryx = "memryx" deepx = "deepx" rknn = "rknn" axengine = "axengine" + axelera = "axelera" synaptics = "synaptics" cpu = "cpu" other = "other" @@ -228,6 +232,7 @@ class HardwareSection(AnalyticsModel): class DetectionModel(AnalyticsModel): + default: bool = metric("Whether the model serves cameras that set no scene") detector: DetectorTypeEnum = metric("Detector type the model runs on") devices: int = count("Devices the model runs on") model_type: ModelTypeEnum = metric("Model architecture") @@ -240,7 +245,7 @@ class DetectionModel(AnalyticsModel): class DetectionSection(AnalyticsModel): - models: dict[SceneEnum, DetectionModel] = metric("Detection models keyed by scene") + models: list[DetectionModel] = metric("Detection models") detection_fps: float = metric("Detections per second across cameras", ge=0) skipped_fps: float = metric("Frames per second skipped across cameras", ge=0) @@ -369,7 +374,8 @@ class FeaturesSection(AnalyticsModel): class NoticeCounts(AnalyticsModel): occurrences: int = count("Occurrences since the last accepted report") - dismissals: int = count("Dismissals since the last accepted report") + acknowledgements: int = count("Acknowledgements since the last accepted report") + mutes: int = count("Mutes since the last accepted report") class HealthSection(AnalyticsModel): diff --git a/frigate/models.py b/frigate/models.py index 8d813d3a66..a5214074fa 100644 --- a/frigate/models.py +++ b/frigate/models.py @@ -209,7 +209,7 @@ class NoticeStats(Model): mutes = IntegerField(default=0) first_seen = DateTimeField() last_seen = DateTimeField() - # watermarks for a future analytics reporter; unused until then + # counts the last accepted analytics report covered reported_occurrences = IntegerField(default=0) reported_acknowledgements = IntegerField(default=0) reported_mutes = IntegerField(default=0) diff --git a/frigate/notices/registry.py b/frigate/notices/registry.py index 0593ed65d3..95049ef2ee 100644 --- a/frigate/notices/registry.py +++ b/frigate/notices/registry.py @@ -395,7 +395,8 @@ class NoticeRegistry: for row in snapshot: NoticeStats.update( reported_occurrences=row["occurrences"], - reported_dismissals=row["dismissals"], + reported_acknowledgements=row["acknowledgements"], + reported_mutes=row["mutes"], ).where(NoticeStats.kind == row["kind"]).execute() def _write_repeats( diff --git a/frigate/test/test_analytics_collectors_detection.py b/frigate/test/test_analytics_collectors_detection.py index d1601e128b..0d9198ca8d 100644 --- a/frigate/test/test_analytics_collectors_detection.py +++ b/frigate/test/test_analytics_collectors_detection.py @@ -7,7 +7,6 @@ from unittest.mock import patch from frigate.analytics.collectors import detection from frigate.analytics.schema import ModelSource -from frigate.detectors.detector_config import SceneEnum from frigate.test.analytics_helpers import make_config, make_context @@ -25,7 +24,8 @@ class TestDetectionCollector(unittest.TestCase): section = detection.collect(make_context(config, stats)) - model = section.models[SceneEnum.all] + [model] = section.models + self.assertTrue(model.default) self.assertEqual(model.detector, "cpu") self.assertEqual(model.devices, 2) self.assertEqual(model.model_type, "ssd") @@ -35,10 +35,29 @@ class TestDetectionCollector(unittest.TestCase): self.assertEqual(section.detection_fps, 12.35) self.assertEqual(section.skipped_fps, 0.0) + def test_named_scene_reports_no_name(self): + config = make_config( + { + "models": [ + {"devices": ["cpu"]}, + { + "scene": "backyard_thermal", + "devices": ["cpu"], + "path": "/thermal.tflite", + }, + ] + } + ) + + section = detection.collect(make_context(config)) + + self.assertEqual([m.default for m in section.models], [True, False]) + self.assertNotIn("backyard_thermal", section.model_dump_json()) + def test_inference_is_null_before_the_first_stats(self): section = detection.collect(make_context()) - self.assertIsNone(section.models[SceneEnum.all].inference_ms) + self.assertIsNone(section.models[0].inference_ms) def test_model_source(self): with tempfile.TemporaryDirectory() as cache: diff --git a/frigate/test/test_analytics_collectors_features.py b/frigate/test/test_analytics_collectors_features.py index 841ac76040..6eb111fbf0 100644 --- a/frigate/test/test_analytics_collectors_features.py +++ b/frigate/test/test_analytics_collectors_features.py @@ -4,8 +4,8 @@ import logging import os import unittest +from peewee import SqliteDatabase from peewee_migrate import Router -from playhouse.sqlite_ext import SqliteExtDatabase from playhouse.sqliteq import SqliteQueueDatabase from frigate.analytics.collectors import features @@ -49,7 +49,7 @@ CONFIG = { class TestFeaturesCollector(unittest.TestCase): def setUp(self): - migrate_db = SqliteExtDatabase(TEST_DB) + migrate_db = SqliteDatabase(TEST_DB) del logging.getLogger("peewee_migrate").handlers[:] Router(migrate_db).run() migrate_db.close() diff --git a/frigate/test/test_analytics_collectors_health.py b/frigate/test/test_analytics_collectors_health.py index b33ed64e68..00118d8b6f 100644 --- a/frigate/test/test_analytics_collectors_health.py +++ b/frigate/test/test_analytics_collectors_health.py @@ -21,23 +21,29 @@ class TestHealthCollector(unittest.TestCase): { "kind": "detector_stuck", "occurrences": 5, - "dismissals": 1, + "acknowledgements": 1, + "mutes": 0, "reported_occurrences": 3, - "reported_dismissals": 1, + "reported_acknowledgements": 1, + "reported_mutes": 0, }, { "kind": "shm_too_low", "occurrences": 2, - "dismissals": 0, + "acknowledgements": 0, + "mutes": 0, "reported_occurrences": 2, - "reported_dismissals": 0, + "reported_acknowledgements": 0, + "reported_mutes": 0, }, { "kind": "update_available", "occurrences": 4, - "dismissals": 0, + "acknowledgements": 0, + "mutes": 0, "reported_occurrences": 0, - "reported_dismissals": 0, + "reported_acknowledgements": 0, + "reported_mutes": 0, }, ] @@ -49,10 +55,10 @@ class TestHealthCollector(unittest.TestCase): self.assertTrue(section.retention_unmet) self.assertEqual( { - kind: (c.occurrences, c.dismissals) + kind: (c.occurrences, c.acknowledgements, c.mutes) for kind, c in section.notices.items() }, - {"detector_stuck": (2, 0)}, + {"detector_stuck": (2, 0, 0)}, ) def test_missing_stats_give_zeros(self): diff --git a/frigate/test/test_analytics_report.py b/frigate/test/test_analytics_report.py index 8f88818b5f..4669d04ea8 100644 --- a/frigate/test/test_analytics_report.py +++ b/frigate/test/test_analytics_report.py @@ -6,8 +6,8 @@ import os import unittest from unittest.mock import Mock, patch +from peewee import SqliteDatabase from peewee_migrate import Router -from playhouse.sqlite_ext import SqliteExtDatabase from playhouse.sqliteq import SqliteQueueDatabase from frigate.analytics import report @@ -50,7 +50,7 @@ class TestBuildReport(unittest.TestCase): class TestRealCollectors(unittest.TestCase): def setUp(self): - migrate_db = SqliteExtDatabase(TEST_DB) + migrate_db = SqliteDatabase(TEST_DB) del logging.getLogger("peewee_migrate").handlers[:] Router(migrate_db).run() migrate_db.close() diff --git a/frigate/test/test_analytics_reporter.py b/frigate/test/test_analytics_reporter.py index 3d12c3628d..bc16e41f08 100644 --- a/frigate/test/test_analytics_reporter.py +++ b/frigate/test/test_analytics_reporter.py @@ -24,9 +24,11 @@ SNAPSHOT = [ { "kind": "detector_stuck", "occurrences": 3, - "dismissals": 0, + "acknowledgements": 0, + "mutes": 0, "reported_occurrences": 1, - "reported_dismissals": 0, + "reported_acknowledgements": 0, + "reported_mutes": 0, } ] diff --git a/frigate/test/test_notice_registry.py b/frigate/test/test_notice_registry.py index 5b9b0559b0..02ab10d08e 100644 --- a/frigate/test/test_notice_registry.py +++ b/frigate/test/test_notice_registry.py @@ -372,7 +372,8 @@ class TestMarkReported(RegistryTestCase): row = self.registry.stats()[0] self.assertEqual(row["occurrences"], 2) self.assertEqual(row["reported_occurrences"], 1) - self.assertEqual(row["reported_dismissals"], 0) + self.assertEqual(row["reported_acknowledgements"], 0) + self.assertEqual(row["reported_mutes"], 0) class TestApply(RegistryTestCase):