adapt analytics to free-form model scenes, split notice counts, and peewee 4

This commit is contained in:
Josh Hawkins
2026-10-29 16:17:20 -05:00
parent 7f65087609
commit d3bc434e2d
12 changed files with 111 additions and 60 deletions
+12 -9
View File
@@ -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(
+6 -3
View File
@@ -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
+9 -3
View File
@@ -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):
+1 -1
View File
@@ -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)
+2 -1
View File
@@ -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(
@@ -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:
@@ -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()
@@ -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):
+2 -2
View File
@@ -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()
+4 -2
View File
@@ -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,
}
]
+2 -1
View File
@@ -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):