mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-05 10:45:21 +03:00
Merge remote-tracking branch 'upstream/dev' into patch-230428
This commit is contained in:
commit
709b2c62d4
@ -5,13 +5,11 @@ title: Frigate+
|
|||||||
|
|
||||||
:::info
|
:::info
|
||||||
|
|
||||||
Frigate+ is under active development and currently only offers the ability to submit your examples with annotations. Models will be available after enough examples are submitted to train a robust model. It is free to create an account and upload your examples.
|
Frigate+ is under active development. Models are available as a part of an invitation only beta. It is free to create an account and upload/annotate your examples.
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
Frigate+ offers models trained from scratch and specifically designed for the way Frigate NVR analyzes video footage. They offer higher accuracy with less resources. By uploading your own labeled examples, your model can be uniquely tuned for accuracy in your specific conditions. After tuning, performance is evaluated against a broad dataset and real world examples submitted by other Frigate+ users to prevent overfitting.
|
Frigate+ offers models trained from scratch and specifically designed for the way Frigate NVR analyzes video footage. They offer higher accuracy with less resources and include a more relevant set of objects for security cameras. By uploading your own labeled examples, your model can be uniquely tuned for accuracy in your specific conditions. After tuning, performance is evaluated against a broad dataset and real world examples submitted by other Frigate+ users to prevent overfitting.
|
||||||
|
|
||||||
Custom models also include a more relevant set of objects for security cameras such as person, face, car, license plate, delivery truck, package, dog, cat, deer, and more. Interested in detecting an object unique to you? Upload examples to incorporate your own objects without worrying that you are reducing the accuracy of other object types in the model.
|
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
@ -35,7 +33,7 @@ You cannot use the `environment_vars` section of your configuration file to set
|
|||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### Submit examples
|
## Submit examples
|
||||||
|
|
||||||
Once your API key is configured, you can submit examples directly from the events page in Frigate using the `SEND TO FRIGATE+` button.
|
Once your API key is configured, you can submit examples directly from the events page in Frigate using the `SEND TO FRIGATE+` button.
|
||||||
|
|
||||||
@ -52,3 +50,25 @@ Snapshots must be enabled to be able to submit examples to Frigate+
|
|||||||
You can view all of your submitted images at [https://plus.frigate.video](https://plus.frigate.video). Annotations can be added by clicking an image.
|
You can view all of your submitted images at [https://plus.frigate.video](https://plus.frigate.video). Annotations can be added by clicking an image.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## Use Models
|
||||||
|
|
||||||
|
Models available in Frigate+ can be used with a special model path. No other information needs to be configured for Frigate+ models because it fetches the remaining config from Frigate+ automatically.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
model:
|
||||||
|
path: plus://e63b7345cc83a84ed79dedfc99c16616
|
||||||
|
```
|
||||||
|
|
||||||
|
Models are downloaded into the `/config/model_cache` folder and only downloaded if needed.
|
||||||
|
|
||||||
|
You can override the labelmap for Frigate+ models like this:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
model:
|
||||||
|
path: plus://e63b7345cc83a84ed79dedfc99c16616
|
||||||
|
labelmap:
|
||||||
|
3: animal
|
||||||
|
4: animal
|
||||||
|
5: animal
|
||||||
|
```
|
||||||
|
|||||||
@ -18,7 +18,14 @@ from frigate.comms.dispatcher import Communicator, Dispatcher
|
|||||||
from frigate.comms.mqtt import MqttClient
|
from frigate.comms.mqtt import MqttClient
|
||||||
from frigate.comms.ws import WebSocketClient
|
from frigate.comms.ws import WebSocketClient
|
||||||
from frigate.config import FrigateConfig
|
from frigate.config import FrigateConfig
|
||||||
from frigate.const import CACHE_DIR, CLIPS_DIR, CONFIG_DIR, DEFAULT_DB_PATH, RECORD_DIR
|
from frigate.const import (
|
||||||
|
CACHE_DIR,
|
||||||
|
CLIPS_DIR,
|
||||||
|
CONFIG_DIR,
|
||||||
|
DEFAULT_DB_PATH,
|
||||||
|
MODEL_CACHE_DIR,
|
||||||
|
RECORD_DIR,
|
||||||
|
)
|
||||||
from frigate.object_detection import ObjectDetectProcess
|
from frigate.object_detection import ObjectDetectProcess
|
||||||
from frigate.events import EventCleanup, EventProcessor
|
from frigate.events import EventCleanup, EventProcessor
|
||||||
from frigate.http import create_app
|
from frigate.http import create_app
|
||||||
@ -57,7 +64,7 @@ class FrigateApp:
|
|||||||
os.environ[key] = value
|
os.environ[key] = value
|
||||||
|
|
||||||
def ensure_dirs(self) -> None:
|
def ensure_dirs(self) -> None:
|
||||||
for d in [CONFIG_DIR, RECORD_DIR, CLIPS_DIR, CACHE_DIR]:
|
for d in [CONFIG_DIR, RECORD_DIR, CLIPS_DIR, CACHE_DIR, MODEL_CACHE_DIR]:
|
||||||
if not os.path.exists(d) and not os.path.islink(d):
|
if not os.path.exists(d) and not os.path.islink(d):
|
||||||
logger.info(f"Creating directory: {d}")
|
logger.info(f"Creating directory: {d}")
|
||||||
os.makedirs(d)
|
os.makedirs(d)
|
||||||
@ -81,7 +88,7 @@ class FrigateApp:
|
|||||||
config_file = config_file_yaml
|
config_file = config_file_yaml
|
||||||
|
|
||||||
user_config = FrigateConfig.parse_file(config_file)
|
user_config = FrigateConfig.parse_file(config_file)
|
||||||
self.config = user_config.runtime_config
|
self.config = user_config.runtime_config(self.plus_api)
|
||||||
|
|
||||||
for camera_name in self.config.cameras.keys():
|
for camera_name in self.config.cameras.keys():
|
||||||
# create camera_metrics
|
# create camera_metrics
|
||||||
@ -379,6 +386,7 @@ class FrigateApp:
|
|||||||
self.init_logger()
|
self.init_logger()
|
||||||
logger.info(f"Starting Frigate ({VERSION})")
|
logger.info(f"Starting Frigate ({VERSION})")
|
||||||
try:
|
try:
|
||||||
|
self.ensure_dirs()
|
||||||
try:
|
try:
|
||||||
self.init_config()
|
self.init_config()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -399,7 +407,6 @@ class FrigateApp:
|
|||||||
self.log_process.terminate()
|
self.log_process.terminate()
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
self.set_environment_vars()
|
self.set_environment_vars()
|
||||||
self.ensure_dirs()
|
|
||||||
self.set_log_levels()
|
self.set_log_levels()
|
||||||
self.init_queues()
|
self.init_queues()
|
||||||
self.init_database()
|
self.init_database()
|
||||||
|
|||||||
@ -19,6 +19,7 @@ from frigate.const import (
|
|||||||
YAML_EXT,
|
YAML_EXT,
|
||||||
)
|
)
|
||||||
from frigate.detectors.detector_config import BaseDetectorConfig
|
from frigate.detectors.detector_config import BaseDetectorConfig
|
||||||
|
from frigate.plus import PlusApi
|
||||||
from frigate.util import (
|
from frigate.util import (
|
||||||
create_mask,
|
create_mask,
|
||||||
deep_merge,
|
deep_merge,
|
||||||
@ -269,6 +270,9 @@ class DetectConfig(FrigateBaseModel):
|
|||||||
default_factory=StationaryConfig,
|
default_factory=StationaryConfig,
|
||||||
title="Stationary objects config.",
|
title="Stationary objects config.",
|
||||||
)
|
)
|
||||||
|
annotation_offset: int = Field(
|
||||||
|
default=0, title="Milliseconds to offset detect annotations by."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FilterConfig(FrigateBaseModel):
|
class FilterConfig(FrigateBaseModel):
|
||||||
@ -903,8 +907,7 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
title="Global timestamp style configuration.",
|
title="Global timestamp style configuration.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
def runtime_config(self, plus_api: PlusApi = None) -> FrigateConfig:
|
||||||
def runtime_config(self) -> FrigateConfig:
|
|
||||||
"""Merge camera config with globals."""
|
"""Merge camera config with globals."""
|
||||||
config = self.copy(deep=True)
|
config = self.copy(deep=True)
|
||||||
|
|
||||||
@ -1028,6 +1031,7 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
enabled_labels.update(camera.objects.track)
|
enabled_labels.update(camera.objects.track)
|
||||||
|
|
||||||
config.model.create_colormap(sorted(enabled_labels))
|
config.model.create_colormap(sorted(enabled_labels))
|
||||||
|
config.model.check_and_load_plus_model(plus_api)
|
||||||
|
|
||||||
for key, detector in config.detectors.items():
|
for key, detector in config.detectors.items():
|
||||||
detector_config: DetectorConfig = parse_obj_as(DetectorConfig, detector)
|
detector_config: DetectorConfig = parse_obj_as(DetectorConfig, detector)
|
||||||
@ -1060,6 +1064,9 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
merged_model["path"] = "/edgetpu_model.tflite"
|
merged_model["path"] = "/edgetpu_model.tflite"
|
||||||
|
|
||||||
detector_config.model = ModelConfig.parse_obj(merged_model)
|
detector_config.model = ModelConfig.parse_obj(merged_model)
|
||||||
|
detector_config.model.check_and_load_plus_model(
|
||||||
|
plus_api, detector_config.type
|
||||||
|
)
|
||||||
detector_config.model.compute_model_hash()
|
detector_config.model.compute_model_hash()
|
||||||
config.detectors[key] = detector_config
|
config.detectors[key] = detector_config
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
CONFIG_DIR = "/config"
|
CONFIG_DIR = "/config"
|
||||||
DEFAULT_DB_PATH = f"{CONFIG_DIR}/frigate.db"
|
DEFAULT_DB_PATH = f"{CONFIG_DIR}/frigate.db"
|
||||||
|
MODEL_CACHE_DIR = f"{CONFIG_DIR}/model_cache"
|
||||||
BASE_DIR = "/media/frigate"
|
BASE_DIR = "/media/frigate"
|
||||||
CLIPS_DIR = f"{BASE_DIR}/clips"
|
CLIPS_DIR = f"{BASE_DIR}/clips"
|
||||||
RECORD_DIR = f"{BASE_DIR}/recordings"
|
RECORD_DIR = f"{BASE_DIR}/recordings"
|
||||||
|
|||||||
@ -1,11 +1,16 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
import os
|
||||||
from typing import Dict, List, Optional, Tuple, Union, Literal
|
from typing import Dict, List, Optional, Tuple, Union, Literal
|
||||||
|
|
||||||
|
|
||||||
|
import requests
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
from pydantic import BaseModel, Extra, Field, validator
|
from pydantic import BaseModel, Extra, Field, validator
|
||||||
from pydantic.fields import PrivateAttr
|
from pydantic.fields import PrivateAttr
|
||||||
|
from frigate.plus import PlusApi
|
||||||
|
|
||||||
from frigate.util import load_labels
|
from frigate.util import load_labels
|
||||||
|
|
||||||
@ -73,6 +78,45 @@ class ModelConfig(BaseModel):
|
|||||||
}
|
}
|
||||||
self._colormap = {}
|
self._colormap = {}
|
||||||
|
|
||||||
|
def check_and_load_plus_model(
|
||||||
|
self, plus_api: PlusApi, detector: str = None
|
||||||
|
) -> None:
|
||||||
|
if not self.path or not self.path.startswith("plus://"):
|
||||||
|
return
|
||||||
|
|
||||||
|
model_id = self.path[7:]
|
||||||
|
self.path = f"/config/model_cache/{model_id}"
|
||||||
|
model_info_path = f"{self.path}.json"
|
||||||
|
|
||||||
|
# download the model if it doesn't exist
|
||||||
|
if not os.path.isfile(self.path):
|
||||||
|
download_url = plus_api.get_model_download_url(model_id)
|
||||||
|
r = requests.get(download_url)
|
||||||
|
with open(self.path, "wb") as f:
|
||||||
|
f.write(r.content)
|
||||||
|
|
||||||
|
# download the model info if it doesn't exist
|
||||||
|
if not os.path.isfile(model_info_path):
|
||||||
|
model_info = plus_api.get_model_info(model_id)
|
||||||
|
with open(model_info_path, "w") as f:
|
||||||
|
json.dump(model_info, f)
|
||||||
|
else:
|
||||||
|
with open(model_info_path, "r") as f:
|
||||||
|
model_info = json.load(f)
|
||||||
|
|
||||||
|
if detector and detector not in model_info["supportedDetectors"]:
|
||||||
|
raise ValueError(f"Model does not support detector type of {detector}")
|
||||||
|
|
||||||
|
self.width = model_info["width"]
|
||||||
|
self.height = model_info["height"]
|
||||||
|
self.input_tensor = model_info["inputShape"]
|
||||||
|
self.input_pixel_format = model_info["pixelFormat"]
|
||||||
|
self.model_type = model_info["type"]
|
||||||
|
self._merged_labelmap = {
|
||||||
|
**{int(key): val for key, val in model_info["labelMap"].items()},
|
||||||
|
**self.labelmap,
|
||||||
|
}
|
||||||
|
|
||||||
def compute_model_hash(self) -> None:
|
def compute_model_hash(self) -> None:
|
||||||
with open(self.path, "rb") as f:
|
with open(self.path, "rb") as f:
|
||||||
file_hash = hashlib.md5()
|
file_hash = hashlib.md5()
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import queue
|
import queue
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from peewee import fn
|
from peewee import fn
|
||||||
@ -10,7 +12,6 @@ from peewee import fn
|
|||||||
from frigate.config import EventsConfig, FrigateConfig
|
from frigate.config import EventsConfig, FrigateConfig
|
||||||
from frigate.const import CLIPS_DIR
|
from frigate.const import CLIPS_DIR
|
||||||
from frigate.models import Event
|
from frigate.models import Event
|
||||||
from frigate.timeline import TimelineSourceEnum
|
|
||||||
from frigate.types import CameraMetricsTypes
|
from frigate.types import CameraMetricsTypes
|
||||||
from frigate.util import to_relative_box
|
from frigate.util import to_relative_box
|
||||||
|
|
||||||
@ -21,6 +22,12 @@ from typing import Dict
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class EventTypeEnum(str, Enum):
|
||||||
|
# api = "api"
|
||||||
|
# audio = "audio"
|
||||||
|
tracked_object = "tracked_object"
|
||||||
|
|
||||||
|
|
||||||
def should_update_db(prev_event: Event, current_event: Event) -> bool:
|
def should_update_db(prev_event: Event, current_event: Event) -> bool:
|
||||||
"""If current_event has updated fields and (clip or snapshot)."""
|
"""If current_event has updated fields and (clip or snapshot)."""
|
||||||
if current_event["has_clip"] or current_event["has_snapshot"]:
|
if current_event["has_clip"] or current_event["has_snapshot"]:
|
||||||
@ -66,7 +73,9 @@ class EventProcessor(threading.Thread):
|
|||||||
|
|
||||||
while not self.stop_event.is_set():
|
while not self.stop_event.is_set():
|
||||||
try:
|
try:
|
||||||
event_type, camera, event_data = self.event_queue.get(timeout=1)
|
source_type, event_type, camera, event_data = self.event_queue.get(
|
||||||
|
timeout=1
|
||||||
|
)
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -75,100 +84,19 @@ class EventProcessor(threading.Thread):
|
|||||||
self.timeline_queue.put(
|
self.timeline_queue.put(
|
||||||
(
|
(
|
||||||
camera,
|
camera,
|
||||||
TimelineSourceEnum.tracked_object,
|
source_type,
|
||||||
event_type,
|
event_type,
|
||||||
self.events_in_process.get(event_data["id"]),
|
self.events_in_process.get(event_data["id"]),
|
||||||
event_data,
|
event_data,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# if this is the first message, just store it and continue, its not time to insert it in the db
|
if source_type == EventTypeEnum.tracked_object:
|
||||||
if event_type == "start":
|
if event_type == "start":
|
||||||
self.events_in_process[event_data["id"]] = event_data
|
self.events_in_process[event_data["id"]] = event_data
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if should_update_db(self.events_in_process[event_data["id"]], event_data):
|
self.handle_object_detection(event_type, camera, event_data)
|
||||||
camera_config = self.config.cameras[camera]
|
|
||||||
event_config: EventsConfig = camera_config.record.events
|
|
||||||
width = camera_config.detect.width
|
|
||||||
height = camera_config.detect.height
|
|
||||||
first_detector = list(self.config.detectors.values())[0]
|
|
||||||
|
|
||||||
start_time = event_data["start_time"] - event_config.pre_capture
|
|
||||||
end_time = (
|
|
||||||
None
|
|
||||||
if event_data["end_time"] is None
|
|
||||||
else event_data["end_time"] + event_config.post_capture
|
|
||||||
)
|
|
||||||
# score of the snapshot
|
|
||||||
score = (
|
|
||||||
None
|
|
||||||
if event_data["snapshot"] is None
|
|
||||||
else event_data["snapshot"]["score"]
|
|
||||||
)
|
|
||||||
# detection region in the snapshot
|
|
||||||
region = (
|
|
||||||
None
|
|
||||||
if event_data["snapshot"] is None
|
|
||||||
else to_relative_box(
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
event_data["snapshot"]["region"],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
# bounding box for the snapshot
|
|
||||||
box = (
|
|
||||||
None
|
|
||||||
if event_data["snapshot"] is None
|
|
||||||
else to_relative_box(
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
event_data["snapshot"]["box"],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# keep these from being set back to false because the event
|
|
||||||
# may have started while recordings and snapshots were enabled
|
|
||||||
# this would be an issue for long running events
|
|
||||||
if self.events_in_process[event_data["id"]]["has_clip"]:
|
|
||||||
event_data["has_clip"] = True
|
|
||||||
if self.events_in_process[event_data["id"]]["has_snapshot"]:
|
|
||||||
event_data["has_snapshot"] = True
|
|
||||||
|
|
||||||
event = {
|
|
||||||
Event.id: event_data["id"],
|
|
||||||
Event.label: event_data["label"],
|
|
||||||
Event.camera: camera,
|
|
||||||
Event.start_time: start_time,
|
|
||||||
Event.end_time: end_time,
|
|
||||||
Event.top_score: event_data["top_score"],
|
|
||||||
Event.score: score,
|
|
||||||
Event.zones: list(event_data["entered_zones"]),
|
|
||||||
Event.thumbnail: event_data["thumbnail"],
|
|
||||||
Event.region: region,
|
|
||||||
Event.box: box,
|
|
||||||
Event.has_clip: event_data["has_clip"],
|
|
||||||
Event.has_snapshot: event_data["has_snapshot"],
|
|
||||||
Event.model_hash: first_detector.model.model_hash,
|
|
||||||
Event.model_type: first_detector.model.model_type,
|
|
||||||
Event.detector_type: first_detector.type,
|
|
||||||
}
|
|
||||||
|
|
||||||
(
|
|
||||||
Event.insert(event)
|
|
||||||
.on_conflict(
|
|
||||||
conflict_target=[Event.id],
|
|
||||||
update=event,
|
|
||||||
)
|
|
||||||
.execute()
|
|
||||||
)
|
|
||||||
|
|
||||||
# update the stored copy for comparison on future update messages
|
|
||||||
self.events_in_process[event_data["id"]] = event_data
|
|
||||||
|
|
||||||
if event_type == "end":
|
|
||||||
del self.events_in_process[event_data["id"]]
|
|
||||||
self.event_processed_queue.put((event_data["id"], camera))
|
|
||||||
|
|
||||||
# set an end_time on events without an end_time before exiting
|
# set an end_time on events without an end_time before exiting
|
||||||
Event.update(end_time=datetime.datetime.now().timestamp()).where(
|
Event.update(end_time=datetime.datetime.now().timestamp()).where(
|
||||||
@ -176,6 +104,99 @@ class EventProcessor(threading.Thread):
|
|||||||
).execute()
|
).execute()
|
||||||
logger.info(f"Exiting event processor...")
|
logger.info(f"Exiting event processor...")
|
||||||
|
|
||||||
|
def handle_object_detection(
|
||||||
|
self,
|
||||||
|
event_type: str,
|
||||||
|
camera: str,
|
||||||
|
event_data: Event,
|
||||||
|
) -> None:
|
||||||
|
"""handle tracked object event updates."""
|
||||||
|
# if this is the first message, just store it and continue, its not time to insert it in the db
|
||||||
|
if should_update_db(self.events_in_process[event_data["id"]], event_data):
|
||||||
|
camera_config = self.config.cameras[camera]
|
||||||
|
event_config: EventsConfig = camera_config.record.events
|
||||||
|
width = camera_config.detect.width
|
||||||
|
height = camera_config.detect.height
|
||||||
|
first_detector = list(self.config.detectors.values())[0]
|
||||||
|
|
||||||
|
start_time = event_data["start_time"] - event_config.pre_capture
|
||||||
|
end_time = (
|
||||||
|
None
|
||||||
|
if event_data["end_time"] is None
|
||||||
|
else event_data["end_time"] + event_config.post_capture
|
||||||
|
)
|
||||||
|
# score of the snapshot
|
||||||
|
score = (
|
||||||
|
None
|
||||||
|
if event_data["snapshot"] is None
|
||||||
|
else event_data["snapshot"]["score"]
|
||||||
|
)
|
||||||
|
# detection region in the snapshot
|
||||||
|
region = (
|
||||||
|
None
|
||||||
|
if event_data["snapshot"] is None
|
||||||
|
else to_relative_box(
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
event_data["snapshot"]["region"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# bounding box for the snapshot
|
||||||
|
box = (
|
||||||
|
None
|
||||||
|
if event_data["snapshot"] is None
|
||||||
|
else to_relative_box(
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
event_data["snapshot"]["box"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# keep these from being set back to false because the event
|
||||||
|
# may have started while recordings and snapshots were enabled
|
||||||
|
# this would be an issue for long running events
|
||||||
|
if self.events_in_process[event_data["id"]]["has_clip"]:
|
||||||
|
event_data["has_clip"] = True
|
||||||
|
if self.events_in_process[event_data["id"]]["has_snapshot"]:
|
||||||
|
event_data["has_snapshot"] = True
|
||||||
|
|
||||||
|
event = {
|
||||||
|
Event.id: event_data["id"],
|
||||||
|
Event.label: event_data["label"],
|
||||||
|
Event.camera: camera,
|
||||||
|
Event.start_time: start_time,
|
||||||
|
Event.end_time: end_time,
|
||||||
|
Event.zones: list(event_data["entered_zones"]),
|
||||||
|
Event.thumbnail: event_data["thumbnail"],
|
||||||
|
Event.has_clip: event_data["has_clip"],
|
||||||
|
Event.has_snapshot: event_data["has_snapshot"],
|
||||||
|
Event.model_hash: first_detector.model.model_hash,
|
||||||
|
Event.model_type: first_detector.model.model_type,
|
||||||
|
Event.detector_type: first_detector.type,
|
||||||
|
Event.data: {
|
||||||
|
"box": box,
|
||||||
|
"region": region,
|
||||||
|
"score": score,
|
||||||
|
"top_score": event_data["top_score"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
(
|
||||||
|
Event.insert(event)
|
||||||
|
.on_conflict(
|
||||||
|
conflict_target=[Event.id],
|
||||||
|
update=event,
|
||||||
|
)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
|
||||||
|
# update the stored copy for comparison on future update messages
|
||||||
|
self.events_in_process[event_data["id"]] = event_data
|
||||||
|
|
||||||
|
if event_type == "end":
|
||||||
|
del self.events_in_process[event_data["id"]]
|
||||||
|
self.event_processed_queue.put((event_data["id"], camera))
|
||||||
|
|
||||||
|
|
||||||
class EventCleanup(threading.Thread):
|
class EventCleanup(threading.Thread):
|
||||||
def __init__(self, config: FrigateConfig, stop_event: MpEvent):
|
def __init__(self, config: FrigateConfig, stop_event: MpEvent):
|
||||||
|
|||||||
@ -44,7 +44,6 @@ from frigate.util import (
|
|||||||
restart_frigate,
|
restart_frigate,
|
||||||
vainfo_hwaccel,
|
vainfo_hwaccel,
|
||||||
get_tz_modifiers,
|
get_tz_modifiers,
|
||||||
to_relative_box,
|
|
||||||
)
|
)
|
||||||
from frigate.storage import StorageMaintainer
|
from frigate.storage import StorageMaintainer
|
||||||
from frigate.version import VERSION
|
from frigate.version import VERSION
|
||||||
@ -196,7 +195,7 @@ def send_to_plus(id):
|
|||||||
return make_response(jsonify({"success": False, "message": message}), 404)
|
return make_response(jsonify({"success": False, "message": message}), 404)
|
||||||
|
|
||||||
# events from before the conversion to relative dimensions cant include annotations
|
# events from before the conversion to relative dimensions cant include annotations
|
||||||
if any(d > 1 for d in event.box):
|
if any(d > 1 for d in event.data["box"]):
|
||||||
include_annotation = None
|
include_annotation = None
|
||||||
|
|
||||||
if event.end_time is None:
|
if event.end_time is None:
|
||||||
@ -252,8 +251,8 @@ def send_to_plus(id):
|
|||||||
event.save()
|
event.save()
|
||||||
|
|
||||||
if not include_annotation is None:
|
if not include_annotation is None:
|
||||||
region = event.region
|
region = event.data["region"]
|
||||||
box = event.box
|
box = event.data["box"]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
current_app.plus_api.add_annotation(
|
current_app.plus_api.add_annotation(
|
||||||
@ -294,7 +293,7 @@ def false_positive(id):
|
|||||||
return make_response(jsonify({"success": False, "message": message}), 404)
|
return make_response(jsonify({"success": False, "message": message}), 404)
|
||||||
|
|
||||||
# events from before the conversion to relative dimensions cant include annotations
|
# events from before the conversion to relative dimensions cant include annotations
|
||||||
if any(d > 1 for d in event.box):
|
if any(d > 1 for d in event.data["box"]):
|
||||||
message = f"Events prior to 0.13 cannot be submitted as false positives"
|
message = f"Events prior to 0.13 cannot be submitted as false positives"
|
||||||
logger.error(message)
|
logger.error(message)
|
||||||
return make_response(jsonify({"success": False, "message": message}), 400)
|
return make_response(jsonify({"success": False, "message": message}), 400)
|
||||||
@ -311,11 +310,15 @@ def false_positive(id):
|
|||||||
# need to refetch the event now that it has a plus_id
|
# need to refetch the event now that it has a plus_id
|
||||||
event = Event.get(Event.id == id)
|
event = Event.get(Event.id == id)
|
||||||
|
|
||||||
region = event.region
|
region = event.data["region"]
|
||||||
box = event.box
|
box = event.data["box"]
|
||||||
|
|
||||||
# provide top score if score is unavailable
|
# provide top score if score is unavailable
|
||||||
score = event.top_score if event.score is None else event.score
|
score = (
|
||||||
|
(event.data["top_score"] if event.data["top_score"] else event.top_score)
|
||||||
|
if event.data["score"] is None
|
||||||
|
else event.data["score"]
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
current_app.plus_api.add_false_positive(
|
current_app.plus_api.add_false_positive(
|
||||||
@ -756,6 +759,7 @@ def events():
|
|||||||
Event.top_score,
|
Event.top_score,
|
||||||
Event.false_positive,
|
Event.false_positive,
|
||||||
Event.box,
|
Event.box,
|
||||||
|
Event.data,
|
||||||
]
|
]
|
||||||
|
|
||||||
if camera != "all":
|
if camera != "all":
|
||||||
@ -862,6 +866,11 @@ def config():
|
|||||||
|
|
||||||
config["plus"] = {"enabled": current_app.plus_api.is_active()}
|
config["plus"] = {"enabled": current_app.plus_api.is_active()}
|
||||||
|
|
||||||
|
for detector, detector_config in config["detectors"].items():
|
||||||
|
detector_config["model"][
|
||||||
|
"labelmap"
|
||||||
|
] = current_app.frigate_config.model.merged_labelmap
|
||||||
|
|
||||||
return jsonify(config)
|
return jsonify(config)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -18,22 +18,33 @@ class Event(Model): # type: ignore[misc]
|
|||||||
camera = CharField(index=True, max_length=20)
|
camera = CharField(index=True, max_length=20)
|
||||||
start_time = DateTimeField()
|
start_time = DateTimeField()
|
||||||
end_time = DateTimeField()
|
end_time = DateTimeField()
|
||||||
top_score = FloatField()
|
top_score = (
|
||||||
score = FloatField()
|
FloatField()
|
||||||
|
) # TODO remove when columns can be dropped without rebuilding table
|
||||||
|
score = (
|
||||||
|
FloatField()
|
||||||
|
) # TODO remove when columns can be dropped without rebuilding table
|
||||||
false_positive = BooleanField()
|
false_positive = BooleanField()
|
||||||
zones = JSONField()
|
zones = JSONField()
|
||||||
thumbnail = TextField()
|
thumbnail = TextField()
|
||||||
has_clip = BooleanField(default=True)
|
has_clip = BooleanField(default=True)
|
||||||
has_snapshot = BooleanField(default=True)
|
has_snapshot = BooleanField(default=True)
|
||||||
region = JSONField()
|
region = (
|
||||||
box = JSONField()
|
JSONField()
|
||||||
area = IntegerField()
|
) # TODO remove when columns can be dropped without rebuilding table
|
||||||
|
box = (
|
||||||
|
JSONField()
|
||||||
|
) # TODO remove when columns can be dropped without rebuilding table
|
||||||
|
area = (
|
||||||
|
IntegerField()
|
||||||
|
) # TODO remove when columns can be dropped without rebuilding table
|
||||||
retain_indefinitely = BooleanField(default=False)
|
retain_indefinitely = BooleanField(default=False)
|
||||||
ratio = FloatField(default=1.0)
|
ratio = FloatField(default=1.0)
|
||||||
plus_id = CharField(max_length=30)
|
plus_id = CharField(max_length=30)
|
||||||
model_hash = CharField(max_length=32)
|
model_hash = CharField(max_length=32)
|
||||||
detector_type = CharField(max_length=32)
|
detector_type = CharField(max_length=32)
|
||||||
model_type = CharField(max_length=32)
|
model_type = CharField(max_length=32)
|
||||||
|
data = JSONField() # ex: tracked object box, region, etc.
|
||||||
|
|
||||||
|
|
||||||
class Timeline(Model): # type: ignore[misc]
|
class Timeline(Model): # type: ignore[misc]
|
||||||
|
|||||||
@ -21,6 +21,7 @@ from frigate.config import (
|
|||||||
FrigateConfig,
|
FrigateConfig,
|
||||||
)
|
)
|
||||||
from frigate.const import CLIPS_DIR
|
from frigate.const import CLIPS_DIR
|
||||||
|
from frigate.events import EventTypeEnum
|
||||||
from frigate.util import (
|
from frigate.util import (
|
||||||
SharedMemoryFrameManager,
|
SharedMemoryFrameManager,
|
||||||
calculate_region,
|
calculate_region,
|
||||||
@ -656,7 +657,9 @@ class TrackedObjectProcessor(threading.Thread):
|
|||||||
self.last_motion_detected: dict[str, float] = {}
|
self.last_motion_detected: dict[str, float] = {}
|
||||||
|
|
||||||
def start(camera, obj: TrackedObject, current_frame_time):
|
def start(camera, obj: TrackedObject, current_frame_time):
|
||||||
self.event_queue.put(("start", camera, obj.to_dict()))
|
self.event_queue.put(
|
||||||
|
(EventTypeEnum.tracked_object, "start", camera, obj.to_dict())
|
||||||
|
)
|
||||||
|
|
||||||
def update(camera, obj: TrackedObject, current_frame_time):
|
def update(camera, obj: TrackedObject, current_frame_time):
|
||||||
obj.has_snapshot = self.should_save_snapshot(camera, obj)
|
obj.has_snapshot = self.should_save_snapshot(camera, obj)
|
||||||
@ -670,7 +673,12 @@ class TrackedObjectProcessor(threading.Thread):
|
|||||||
self.dispatcher.publish("events", json.dumps(message), retain=False)
|
self.dispatcher.publish("events", json.dumps(message), retain=False)
|
||||||
obj.previous = after
|
obj.previous = after
|
||||||
self.event_queue.put(
|
self.event_queue.put(
|
||||||
("update", camera, obj.to_dict(include_thumbnail=True))
|
(
|
||||||
|
EventTypeEnum.tracked_object,
|
||||||
|
"update",
|
||||||
|
camera,
|
||||||
|
obj.to_dict(include_thumbnail=True),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def end(camera, obj: TrackedObject, current_frame_time):
|
def end(camera, obj: TrackedObject, current_frame_time):
|
||||||
@ -722,7 +730,14 @@ class TrackedObjectProcessor(threading.Thread):
|
|||||||
}
|
}
|
||||||
self.dispatcher.publish("events", json.dumps(message), retain=False)
|
self.dispatcher.publish("events", json.dumps(message), retain=False)
|
||||||
|
|
||||||
self.event_queue.put(("end", camera, obj.to_dict(include_thumbnail=True)))
|
self.event_queue.put(
|
||||||
|
(
|
||||||
|
EventTypeEnum.tracked_object,
|
||||||
|
"end",
|
||||||
|
camera,
|
||||||
|
obj.to_dict(include_thumbnail=True),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def snapshot(camera, obj: TrackedObject, current_frame_time):
|
def snapshot(camera, obj: TrackedObject, current_frame_time):
|
||||||
mqtt_config: MqttConfig = self.config.cameras[camera].mqtt
|
mqtt_config: MqttConfig = self.config.cameras[camera].mqtt
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from typing import List
|
from typing import Any, Dict, List
|
||||||
import requests
|
import requests
|
||||||
from frigate.const import PLUS_ENV_VAR, PLUS_API_HOST
|
from frigate.const import PLUS_ENV_VAR, PLUS_API_HOST
|
||||||
from requests.models import Response
|
from requests.models import Response
|
||||||
@ -187,3 +187,24 @@ class PlusApi:
|
|||||||
|
|
||||||
if not r.ok:
|
if not r.ok:
|
||||||
raise Exception(r.text)
|
raise Exception(r.text)
|
||||||
|
|
||||||
|
def get_model_download_url(
|
||||||
|
self,
|
||||||
|
model_id: str,
|
||||||
|
) -> str:
|
||||||
|
r = self._get(f"model/{model_id}/signed_url")
|
||||||
|
|
||||||
|
if not r.ok:
|
||||||
|
raise Exception(r.text)
|
||||||
|
|
||||||
|
presigned_url = r.json()
|
||||||
|
|
||||||
|
return str(presigned_url.get("url"))
|
||||||
|
|
||||||
|
def get_model_info(self, model_id: str) -> Any:
|
||||||
|
r = self._get(f"model/{model_id}")
|
||||||
|
|
||||||
|
if not r.ok:
|
||||||
|
raise Exception(r.text)
|
||||||
|
|
||||||
|
return r.json()
|
||||||
|
|||||||
@ -12,7 +12,7 @@ from multiprocessing.synchronize import Event as MpEvent
|
|||||||
|
|
||||||
from frigate.config import RetainModeEnum, FrigateConfig
|
from frigate.config import RetainModeEnum, FrigateConfig
|
||||||
from frigate.const import RECORD_DIR, SECONDS_IN_DAY
|
from frigate.const import RECORD_DIR, SECONDS_IN_DAY
|
||||||
from frigate.models import Event, Recordings
|
from frigate.models import Event, Recordings, Timeline
|
||||||
from frigate.record.util import remove_empty_directories
|
from frigate.record.util import remove_empty_directories
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -140,6 +140,15 @@ class RecordingCleanup(threading.Thread):
|
|||||||
Path(recording.path).unlink(missing_ok=True)
|
Path(recording.path).unlink(missing_ok=True)
|
||||||
deleted_recordings.add(recording.id)
|
deleted_recordings.add(recording.id)
|
||||||
|
|
||||||
|
# delete timeline entries relevant to this recording segment
|
||||||
|
Timeline.delete().where(
|
||||||
|
Timeline.timestamp.between(
|
||||||
|
recording.start_time, recording.end_time
|
||||||
|
),
|
||||||
|
Timeline.timestamp < expire_date,
|
||||||
|
Timeline.camera == camera,
|
||||||
|
).execute()
|
||||||
|
|
||||||
logger.debug(f"Expiring {len(deleted_recordings)} recordings")
|
logger.debug(f"Expiring {len(deleted_recordings)} recordings")
|
||||||
# delete up to 100,000 at a time
|
# delete up to 100,000 at a time
|
||||||
max_deletes = 100000
|
max_deletes = 100000
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
import unittest
|
import unittest
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
@ -6,7 +8,9 @@ from frigate.config import (
|
|||||||
BirdseyeModeEnum,
|
BirdseyeModeEnum,
|
||||||
FrigateConfig,
|
FrigateConfig,
|
||||||
)
|
)
|
||||||
|
from frigate.const import MODEL_CACHE_DIR
|
||||||
from frigate.detectors import DetectorTypeEnum
|
from frigate.detectors import DetectorTypeEnum
|
||||||
|
from frigate.plus import PlusApi
|
||||||
from frigate.util import deep_merge, load_config_with_no_duplicates
|
from frigate.util import deep_merge, load_config_with_no_duplicates
|
||||||
|
|
||||||
|
|
||||||
@ -30,11 +34,40 @@ class TestConfig(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.plus_model_info = {
|
||||||
|
"id": "e63b7345cc83a84ed79dedfc99c16616",
|
||||||
|
"name": "SSDLite Mobiledet",
|
||||||
|
"description": "Fine tuned model",
|
||||||
|
"trainDate": "2023-04-28T23:22:01.262Z",
|
||||||
|
"type": "ssd",
|
||||||
|
"supportedDetectors": ["edgetpu"],
|
||||||
|
"width": 320,
|
||||||
|
"height": 320,
|
||||||
|
"inputShape": "nhwc",
|
||||||
|
"pixelFormat": "rgb",
|
||||||
|
"labelMap": {
|
||||||
|
"0": "amazon",
|
||||||
|
"1": "car",
|
||||||
|
"2": "cat",
|
||||||
|
"3": "deer",
|
||||||
|
"4": "dog",
|
||||||
|
"5": "face",
|
||||||
|
"6": "fedex",
|
||||||
|
"7": "license_plate",
|
||||||
|
"8": "package",
|
||||||
|
"9": "person",
|
||||||
|
"10": "ups",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if not os.path.exists(MODEL_CACHE_DIR) and not os.path.islink(MODEL_CACHE_DIR):
|
||||||
|
os.makedirs(MODEL_CACHE_DIR)
|
||||||
|
|
||||||
def test_config_class(self):
|
def test_config_class(self):
|
||||||
frigate_config = FrigateConfig(**self.minimal)
|
frigate_config = FrigateConfig(**self.minimal)
|
||||||
assert self.minimal == frigate_config.dict(exclude_unset=True)
|
assert self.minimal == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert "cpu" in runtime_config.detectors.keys()
|
assert "cpu" in runtime_config.detectors.keys()
|
||||||
assert runtime_config.detectors["cpu"].type == DetectorTypeEnum.cpu
|
assert runtime_config.detectors["cpu"].type == DetectorTypeEnum.cpu
|
||||||
assert runtime_config.detectors["cpu"].model.width == 320
|
assert runtime_config.detectors["cpu"].model.width == 320
|
||||||
@ -59,7 +92,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
frigate_config = FrigateConfig(**(deep_merge(config, self.minimal)))
|
frigate_config = FrigateConfig(**(deep_merge(config, self.minimal)))
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
|
|
||||||
assert "cpu" in runtime_config.detectors.keys()
|
assert "cpu" in runtime_config.detectors.keys()
|
||||||
assert "edgetpu" in runtime_config.detectors.keys()
|
assert "edgetpu" in runtime_config.detectors.keys()
|
||||||
@ -125,7 +158,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert "dog" in runtime_config.cameras["back"].objects.track
|
assert "dog" in runtime_config.cameras["back"].objects.track
|
||||||
|
|
||||||
def test_override_birdseye(self):
|
def test_override_birdseye(self):
|
||||||
@ -151,7 +184,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert not runtime_config.cameras["back"].birdseye.enabled
|
assert not runtime_config.cameras["back"].birdseye.enabled
|
||||||
assert runtime_config.cameras["back"].birdseye.mode is BirdseyeModeEnum.motion
|
assert runtime_config.cameras["back"].birdseye.mode is BirdseyeModeEnum.motion
|
||||||
|
|
||||||
@ -177,7 +210,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert runtime_config.cameras["back"].birdseye.enabled
|
assert runtime_config.cameras["back"].birdseye.enabled
|
||||||
|
|
||||||
def test_inherit_birdseye(self):
|
def test_inherit_birdseye(self):
|
||||||
@ -202,7 +235,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert runtime_config.cameras["back"].birdseye.enabled
|
assert runtime_config.cameras["back"].birdseye.enabled
|
||||||
assert (
|
assert (
|
||||||
runtime_config.cameras["back"].birdseye.mode is BirdseyeModeEnum.continuous
|
runtime_config.cameras["back"].birdseye.mode is BirdseyeModeEnum.continuous
|
||||||
@ -231,7 +264,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert "cat" in runtime_config.cameras["back"].objects.track
|
assert "cat" in runtime_config.cameras["back"].objects.track
|
||||||
|
|
||||||
def test_default_object_filters(self):
|
def test_default_object_filters(self):
|
||||||
@ -256,7 +289,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert "dog" in runtime_config.cameras["back"].objects.filters
|
assert "dog" in runtime_config.cameras["back"].objects.filters
|
||||||
|
|
||||||
def test_inherit_object_filters(self):
|
def test_inherit_object_filters(self):
|
||||||
@ -284,7 +317,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert "dog" in runtime_config.cameras["back"].objects.filters
|
assert "dog" in runtime_config.cameras["back"].objects.filters
|
||||||
assert runtime_config.cameras["back"].objects.filters["dog"].threshold == 0.7
|
assert runtime_config.cameras["back"].objects.filters["dog"].threshold == 0.7
|
||||||
|
|
||||||
@ -313,7 +346,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert "dog" in runtime_config.cameras["back"].objects.filters
|
assert "dog" in runtime_config.cameras["back"].objects.filters
|
||||||
assert runtime_config.cameras["back"].objects.filters["dog"].threshold == 0.7
|
assert runtime_config.cameras["back"].objects.filters["dog"].threshold == 0.7
|
||||||
|
|
||||||
@ -343,7 +376,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
back_camera = runtime_config.cameras["back"]
|
back_camera = runtime_config.cameras["back"]
|
||||||
assert "dog" in back_camera.objects.filters
|
assert "dog" in back_camera.objects.filters
|
||||||
assert len(back_camera.objects.filters["dog"].raw_mask) == 2
|
assert len(back_camera.objects.filters["dog"].raw_mask) == 2
|
||||||
@ -374,7 +407,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert "-rtsp_transport" in runtime_config.cameras["back"].ffmpeg_cmds[0]["cmd"]
|
assert "-rtsp_transport" in runtime_config.cameras["back"].ffmpeg_cmds[0]["cmd"]
|
||||||
|
|
||||||
def test_ffmpeg_params_global(self):
|
def test_ffmpeg_params_global(self):
|
||||||
@ -403,7 +436,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert "-re" in runtime_config.cameras["back"].ffmpeg_cmds[0]["cmd"]
|
assert "-re" in runtime_config.cameras["back"].ffmpeg_cmds[0]["cmd"]
|
||||||
|
|
||||||
def test_ffmpeg_params_camera(self):
|
def test_ffmpeg_params_camera(self):
|
||||||
@ -433,7 +466,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert "-re" in runtime_config.cameras["back"].ffmpeg_cmds[0]["cmd"]
|
assert "-re" in runtime_config.cameras["back"].ffmpeg_cmds[0]["cmd"]
|
||||||
assert "test" not in runtime_config.cameras["back"].ffmpeg_cmds[0]["cmd"]
|
assert "test" not in runtime_config.cameras["back"].ffmpeg_cmds[0]["cmd"]
|
||||||
|
|
||||||
@ -468,7 +501,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert "-re" in runtime_config.cameras["back"].ffmpeg_cmds[0]["cmd"]
|
assert "-re" in runtime_config.cameras["back"].ffmpeg_cmds[0]["cmd"]
|
||||||
assert "test" in runtime_config.cameras["back"].ffmpeg_cmds[0]["cmd"]
|
assert "test" in runtime_config.cameras["back"].ffmpeg_cmds[0]["cmd"]
|
||||||
assert "test2" not in runtime_config.cameras["back"].ffmpeg_cmds[0]["cmd"]
|
assert "test2" not in runtime_config.cameras["back"].ffmpeg_cmds[0]["cmd"]
|
||||||
@ -498,7 +531,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert (
|
assert (
|
||||||
runtime_config.cameras["back"].record.events.retain.objects["person"] == 30
|
runtime_config.cameras["back"].record.events.retain.objects["person"] == 30
|
||||||
)
|
)
|
||||||
@ -576,7 +609,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert isinstance(
|
assert isinstance(
|
||||||
runtime_config.cameras["back"].zones["test"].contour, np.ndarray
|
runtime_config.cameras["back"].zones["test"].contour, np.ndarray
|
||||||
)
|
)
|
||||||
@ -608,7 +641,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
back_camera = runtime_config.cameras["back"]
|
back_camera = runtime_config.cameras["back"]
|
||||||
assert back_camera.record.events.objects is None
|
assert back_camera.record.events.objects is None
|
||||||
assert back_camera.record.events.retain.objects["person"] == 30
|
assert back_camera.record.events.retain.objects["person"] == 30
|
||||||
@ -639,7 +672,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
ffmpeg_cmds = runtime_config.cameras["back"].ffmpeg_cmds
|
ffmpeg_cmds = runtime_config.cameras["back"].ffmpeg_cmds
|
||||||
assert len(ffmpeg_cmds) == 1
|
assert len(ffmpeg_cmds) == 1
|
||||||
assert not "clips" in ffmpeg_cmds[0]["roles"]
|
assert not "clips" in ffmpeg_cmds[0]["roles"]
|
||||||
@ -670,7 +703,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert runtime_config.cameras["back"].detect.max_disappeared == 5 * 5
|
assert runtime_config.cameras["back"].detect.max_disappeared == 5 * 5
|
||||||
|
|
||||||
def test_motion_frame_height_wont_go_below_120(self):
|
def test_motion_frame_height_wont_go_below_120(self):
|
||||||
@ -698,7 +731,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert runtime_config.cameras["back"].motion.frame_height == 50
|
assert runtime_config.cameras["back"].motion.frame_height == 50
|
||||||
|
|
||||||
def test_motion_contour_area_dynamic(self):
|
def test_motion_contour_area_dynamic(self):
|
||||||
@ -726,7 +759,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert round(runtime_config.cameras["back"].motion.contour_area) == 30
|
assert round(runtime_config.cameras["back"].motion.contour_area) == 30
|
||||||
|
|
||||||
def test_merge_labelmap(self):
|
def test_merge_labelmap(self):
|
||||||
@ -755,7 +788,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert runtime_config.model.merged_labelmap[7] == "truck"
|
assert runtime_config.model.merged_labelmap[7] == "truck"
|
||||||
|
|
||||||
def test_default_labelmap_empty(self):
|
def test_default_labelmap_empty(self):
|
||||||
@ -783,7 +816,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert runtime_config.model.merged_labelmap[0] == "person"
|
assert runtime_config.model.merged_labelmap[0] == "person"
|
||||||
|
|
||||||
def test_default_labelmap(self):
|
def test_default_labelmap(self):
|
||||||
@ -812,9 +845,43 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert runtime_config.model.merged_labelmap[0] == "person"
|
assert runtime_config.model.merged_labelmap[0] == "person"
|
||||||
|
|
||||||
|
def test_plus_labelmap(self):
|
||||||
|
with open("/config/model_cache/test", "w") as f:
|
||||||
|
json.dump(self.plus_model_info, f)
|
||||||
|
with open("/config/model_cache/test.json", "w") as f:
|
||||||
|
json.dump(self.plus_model_info, f)
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"mqtt": {"host": "mqtt"},
|
||||||
|
"model": {"path": "plus://test"},
|
||||||
|
"cameras": {
|
||||||
|
"back": {
|
||||||
|
"ffmpeg": {
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"path": "rtsp://10.0.0.1:554/video",
|
||||||
|
"roles": ["detect"],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"detect": {
|
||||||
|
"height": 1080,
|
||||||
|
"width": 1920,
|
||||||
|
"fps": 5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
frigate_config = FrigateConfig(**config)
|
||||||
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
|
runtime_config = frigate_config.runtime_config(PlusApi())
|
||||||
|
assert runtime_config.model.merged_labelmap[0] == "amazon"
|
||||||
|
|
||||||
def test_fails_on_invalid_role(self):
|
def test_fails_on_invalid_role(self):
|
||||||
config = {
|
config = {
|
||||||
"mqtt": {"host": "mqtt"},
|
"mqtt": {"host": "mqtt"},
|
||||||
@ -871,7 +938,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
self.assertRaises(ValueError, lambda: frigate_config.runtime_config)
|
self.assertRaises(ValueError, lambda: frigate_config.runtime_config())
|
||||||
|
|
||||||
def test_works_on_missing_role_multiple_cams(self):
|
def test_works_on_missing_role_multiple_cams(self):
|
||||||
config = {
|
config = {
|
||||||
@ -919,7 +986,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
|
|
||||||
def test_global_detect(self):
|
def test_global_detect(self):
|
||||||
config = {
|
config = {
|
||||||
@ -946,7 +1013,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert runtime_config.cameras["back"].detect.max_disappeared == 1
|
assert runtime_config.cameras["back"].detect.max_disappeared == 1
|
||||||
assert runtime_config.cameras["back"].detect.height == 1080
|
assert runtime_config.cameras["back"].detect.height == 1080
|
||||||
|
|
||||||
@ -969,7 +1036,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert runtime_config.cameras["back"].detect.max_disappeared == 25
|
assert runtime_config.cameras["back"].detect.max_disappeared == 25
|
||||||
assert runtime_config.cameras["back"].detect.height == 720
|
assert runtime_config.cameras["back"].detect.height == 720
|
||||||
|
|
||||||
@ -998,7 +1065,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert runtime_config.cameras["back"].detect.max_disappeared == 1
|
assert runtime_config.cameras["back"].detect.max_disappeared == 1
|
||||||
assert runtime_config.cameras["back"].detect.height == 1080
|
assert runtime_config.cameras["back"].detect.height == 1080
|
||||||
assert runtime_config.cameras["back"].detect.width == 1920
|
assert runtime_config.cameras["back"].detect.width == 1920
|
||||||
@ -1026,7 +1093,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert runtime_config.cameras["back"].snapshots.enabled
|
assert runtime_config.cameras["back"].snapshots.enabled
|
||||||
assert runtime_config.cameras["back"].snapshots.height == 100
|
assert runtime_config.cameras["back"].snapshots.height == 100
|
||||||
|
|
||||||
@ -1049,7 +1116,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert runtime_config.cameras["back"].snapshots.bounding_box
|
assert runtime_config.cameras["back"].snapshots.bounding_box
|
||||||
assert runtime_config.cameras["back"].snapshots.quality == 70
|
assert runtime_config.cameras["back"].snapshots.quality == 70
|
||||||
|
|
||||||
@ -1077,7 +1144,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert runtime_config.cameras["back"].snapshots.bounding_box == False
|
assert runtime_config.cameras["back"].snapshots.bounding_box == False
|
||||||
assert runtime_config.cameras["back"].snapshots.height == 150
|
assert runtime_config.cameras["back"].snapshots.height == 150
|
||||||
assert runtime_config.cameras["back"].snapshots.enabled
|
assert runtime_config.cameras["back"].snapshots.enabled
|
||||||
@ -1101,7 +1168,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert not runtime_config.cameras["back"].rtmp.enabled
|
assert not runtime_config.cameras["back"].rtmp.enabled
|
||||||
|
|
||||||
def test_default_not_rtmp(self):
|
def test_default_not_rtmp(self):
|
||||||
@ -1123,7 +1190,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert not runtime_config.cameras["back"].rtmp.enabled
|
assert not runtime_config.cameras["back"].rtmp.enabled
|
||||||
|
|
||||||
def test_global_rtmp_merge(self):
|
def test_global_rtmp_merge(self):
|
||||||
@ -1149,7 +1216,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert runtime_config.cameras["back"].rtmp.enabled
|
assert runtime_config.cameras["back"].rtmp.enabled
|
||||||
|
|
||||||
def test_global_rtmp_default(self):
|
def test_global_rtmp_default(self):
|
||||||
@ -1175,7 +1242,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert not runtime_config.cameras["back"].rtmp.enabled
|
assert not runtime_config.cameras["back"].rtmp.enabled
|
||||||
|
|
||||||
def test_global_jsmpeg(self):
|
def test_global_jsmpeg(self):
|
||||||
@ -1198,7 +1265,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert runtime_config.cameras["back"].live.quality == 4
|
assert runtime_config.cameras["back"].live.quality == 4
|
||||||
|
|
||||||
def test_default_live(self):
|
def test_default_live(self):
|
||||||
@ -1220,7 +1287,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert runtime_config.cameras["back"].live.quality == 8
|
assert runtime_config.cameras["back"].live.quality == 8
|
||||||
|
|
||||||
def test_global_live_merge(self):
|
def test_global_live_merge(self):
|
||||||
@ -1246,7 +1313,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert runtime_config.cameras["back"].live.quality == 7
|
assert runtime_config.cameras["back"].live.quality == 7
|
||||||
assert runtime_config.cameras["back"].live.height == 480
|
assert runtime_config.cameras["back"].live.height == 480
|
||||||
|
|
||||||
@ -1270,7 +1337,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert runtime_config.cameras["back"].timestamp_style.position == "bl"
|
assert runtime_config.cameras["back"].timestamp_style.position == "bl"
|
||||||
|
|
||||||
def test_default_timestamp_style(self):
|
def test_default_timestamp_style(self):
|
||||||
@ -1292,7 +1359,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert runtime_config.cameras["back"].timestamp_style.position == "tl"
|
assert runtime_config.cameras["back"].timestamp_style.position == "tl"
|
||||||
|
|
||||||
def test_global_timestamp_style_merge(self):
|
def test_global_timestamp_style_merge(self):
|
||||||
@ -1317,7 +1384,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert runtime_config.cameras["back"].timestamp_style.position == "bl"
|
assert runtime_config.cameras["back"].timestamp_style.position == "bl"
|
||||||
assert runtime_config.cameras["back"].timestamp_style.thickness == 4
|
assert runtime_config.cameras["back"].timestamp_style.thickness == 4
|
||||||
|
|
||||||
@ -1341,7 +1408,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert runtime_config.cameras["back"].snapshots.retain.default == 1.5
|
assert runtime_config.cameras["back"].snapshots.retain.default == 1.5
|
||||||
|
|
||||||
def test_fails_on_bad_camera_name(self):
|
def test_fails_on_bad_camera_name(self):
|
||||||
@ -1365,7 +1432,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
|
|
||||||
self.assertRaises(
|
self.assertRaises(
|
||||||
ValidationError, lambda: frigate_config.runtime_config.cameras
|
ValidationError, lambda: frigate_config.runtime_config().cameras
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_fails_on_bad_segment_time(self):
|
def test_fails_on_bad_segment_time(self):
|
||||||
@ -1392,7 +1459,8 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
|
|
||||||
self.assertRaises(
|
self.assertRaises(
|
||||||
ValueError, lambda: frigate_config.runtime_config.ffmpeg.output_args.record
|
ValueError,
|
||||||
|
lambda: frigate_config.runtime_config().ffmpeg.output_args.record,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_fails_zone_defines_untracked_object(self):
|
def test_fails_zone_defines_untracked_object(self):
|
||||||
@ -1421,7 +1489,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
|
|
||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
|
|
||||||
self.assertRaises(ValueError, lambda: frigate_config.runtime_config.cameras)
|
self.assertRaises(ValueError, lambda: frigate_config.runtime_config().cameras)
|
||||||
|
|
||||||
def test_fails_duplicate_keys(self):
|
def test_fails_duplicate_keys(self):
|
||||||
raw_config = """
|
raw_config = """
|
||||||
@ -1465,7 +1533,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
frigate_config = FrigateConfig(**config)
|
frigate_config = FrigateConfig(**config)
|
||||||
assert config == frigate_config.dict(exclude_unset=True)
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config()
|
||||||
assert "dog" in runtime_config.cameras["back"].objects.filters
|
assert "dog" in runtime_config.cameras["back"].objects.filters
|
||||||
assert runtime_config.cameras["back"].objects.filters["dog"].min_ratio == 0.2
|
assert runtime_config.cameras["back"].objects.filters["dog"].min_ratio == 0.2
|
||||||
assert runtime_config.cameras["back"].objects.filters["dog"].max_ratio == 10.1
|
assert runtime_config.cameras["back"].objects.filters["dog"].max_ratio == 10.1
|
||||||
|
|||||||
@ -292,7 +292,7 @@ class TestHttp(unittest.TestCase):
|
|||||||
|
|
||||||
def test_config(self):
|
def test_config(self):
|
||||||
app = create_app(
|
app = create_app(
|
||||||
FrigateConfig(**self.minimal_config).runtime_config,
|
FrigateConfig(**self.minimal_config).runtime_config(),
|
||||||
self.db,
|
self.db,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
@ -308,7 +308,7 @@ class TestHttp(unittest.TestCase):
|
|||||||
|
|
||||||
def test_recordings(self):
|
def test_recordings(self):
|
||||||
app = create_app(
|
app = create_app(
|
||||||
FrigateConfig(**self.minimal_config).runtime_config,
|
FrigateConfig(**self.minimal_config).runtime_config(),
|
||||||
self.db,
|
self.db,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
@ -327,7 +327,7 @@ class TestHttp(unittest.TestCase):
|
|||||||
@patch("frigate.http.stats_snapshot")
|
@patch("frigate.http.stats_snapshot")
|
||||||
def test_stats(self, mock_stats):
|
def test_stats(self, mock_stats):
|
||||||
app = create_app(
|
app = create_app(
|
||||||
FrigateConfig(**self.minimal_config).runtime_config,
|
FrigateConfig(**self.minimal_config).runtime_config(),
|
||||||
self.db,
|
self.db,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
|||||||
@ -4,9 +4,8 @@ import logging
|
|||||||
import threading
|
import threading
|
||||||
import queue
|
import queue
|
||||||
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
from frigate.config import FrigateConfig
|
from frigate.config import FrigateConfig
|
||||||
|
from frigate.events import EventTypeEnum
|
||||||
from frigate.models import Timeline
|
from frigate.models import Timeline
|
||||||
|
|
||||||
from multiprocessing.queues import Queue
|
from multiprocessing.queues import Queue
|
||||||
@ -17,12 +16,6 @@ from frigate.util import to_relative_box
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TimelineSourceEnum(str, Enum):
|
|
||||||
# api = "api"
|
|
||||||
# audio = "audio"
|
|
||||||
tracked_object = "tracked_object"
|
|
||||||
|
|
||||||
|
|
||||||
class TimelineProcessor(threading.Thread):
|
class TimelineProcessor(threading.Thread):
|
||||||
"""Handle timeline queue and update DB."""
|
"""Handle timeline queue and update DB."""
|
||||||
|
|
||||||
@ -51,7 +44,7 @@ class TimelineProcessor(threading.Thread):
|
|||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if input_type == TimelineSourceEnum.tracked_object:
|
if input_type == EventTypeEnum.tracked_object:
|
||||||
self.handle_object_detection(
|
self.handle_object_detection(
|
||||||
camera, event_type, prev_event_data, event_data
|
camera, event_type, prev_event_data, event_data
|
||||||
)
|
)
|
||||||
|
|||||||
@ -817,7 +817,13 @@ def get_cpu_stats() -> dict[str, dict]:
|
|||||||
else:
|
else:
|
||||||
mem_pct = round((mem_res / total_mem) * 100, 1)
|
mem_pct = round((mem_res / total_mem) * 100, 1)
|
||||||
|
|
||||||
usages[pid] = {
|
idx = pid
|
||||||
|
if stats[1] == "(go2rtc)":
|
||||||
|
idx = "go2rtc"
|
||||||
|
if stats[1] == "(frigate.r+)":
|
||||||
|
idx = "recording"
|
||||||
|
|
||||||
|
usages[idx] = {
|
||||||
"cpu": str(round(cpu_usage, 2)),
|
"cpu": str(round(cpu_usage, 2)),
|
||||||
"mem": f"{mem_pct}",
|
"mem": f"{mem_pct}",
|
||||||
}
|
}
|
||||||
|
|||||||
49
migrations/015_event_refactor.py
Normal file
49
migrations/015_event_refactor.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
"""Peewee migrations
|
||||||
|
|
||||||
|
Some examples (model - class or model name)::
|
||||||
|
|
||||||
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
|
> migrator.sql(sql) # Run custom SQL
|
||||||
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
> migrator.change_fields(model, **fields) # Change fields
|
||||||
|
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||||
|
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||||
|
> migrator.rename_table(model, new_table_name)
|
||||||
|
> migrator.add_index(model, *col_names, unique=False)
|
||||||
|
> migrator.drop_index(model, *col_names)
|
||||||
|
> migrator.add_not_null(model, *field_names)
|
||||||
|
> migrator.drop_not_null(model, *field_names)
|
||||||
|
> migrator.add_default(model, field_name, default)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime as dt
|
||||||
|
import peewee as pw
|
||||||
|
from playhouse.sqlite_ext import *
|
||||||
|
from decimal import ROUND_HALF_EVEN
|
||||||
|
from frigate.models import Event
|
||||||
|
|
||||||
|
try:
|
||||||
|
import playhouse.postgres_ext as pw_pext
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
SQL = pw.SQL
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(migrator, database, fake=False, **kwargs):
|
||||||
|
migrator.drop_not_null(
|
||||||
|
Event, "top_score", "score", "region", "box", "area", "ratio"
|
||||||
|
)
|
||||||
|
migrator.add_fields(
|
||||||
|
Event,
|
||||||
|
data=JSONField(default={}),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def rollback(migrator, database, fake=False, **kwargs):
|
||||||
|
pass
|
||||||
@ -163,7 +163,9 @@ export function EventCard({ camera, event }) {
|
|||||||
<div className="text-xs md:text-normal text-gray-300">Start: {format(start, 'HH:mm:ss')}</div>
|
<div className="text-xs md:text-normal text-gray-300">Start: {format(start, 'HH:mm:ss')}</div>
|
||||||
<div className="text-xs md:text-normal text-gray-300">Duration: {duration}</div>
|
<div className="text-xs md:text-normal text-gray-300">Duration: {duration}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-lg text-white text-right leading-tight">{(event.top_score * 100).toFixed(1)}%</div>
|
<div className="text-lg text-white text-right leading-tight">
|
||||||
|
{((event?.data?.top_score || event.top_score) * 100).toFixed(1)}%
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { formatUnixTimestampToDateTime } from '../utils/dateUtil';
|
|||||||
import PlayIcon from '../icons/Play';
|
import PlayIcon from '../icons/Play';
|
||||||
import ExitIcon from '../icons/Exit';
|
import ExitIcon from '../icons/Exit';
|
||||||
import { Zone } from '../icons/Zone';
|
import { Zone } from '../icons/Zone';
|
||||||
import { useState } from 'preact/hooks';
|
import { useMemo, useState } from 'preact/hooks';
|
||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
|
|
||||||
export default function TimelineSummary({ event, onFrameSelected }) {
|
export default function TimelineSummary({ event, onFrameSelected }) {
|
||||||
@ -18,6 +18,14 @@ export default function TimelineSummary({ event, onFrameSelected }) {
|
|||||||
|
|
||||||
const { data: config } = useSWR('config');
|
const { data: config } = useSWR('config');
|
||||||
|
|
||||||
|
const annotationOffset = useMemo(() => {
|
||||||
|
if (!config) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (config.cameras[event.camera]?.detect?.annotation_offset || 0) / 1000;
|
||||||
|
}, [config, event]);
|
||||||
|
|
||||||
const [timeIndex, setTimeIndex] = useState(-1);
|
const [timeIndex, setTimeIndex] = useState(-1);
|
||||||
|
|
||||||
const recordingParams = {
|
const recordingParams = {
|
||||||
@ -53,7 +61,7 @@ export default function TimelineSummary({ event, onFrameSelected }) {
|
|||||||
|
|
||||||
const onSelectMoment = async (index) => {
|
const onSelectMoment = async (index) => {
|
||||||
setTimeIndex(index);
|
setTimeIndex(index);
|
||||||
onFrameSelected(eventTimeline[index], getSeekSeconds(eventTimeline[index].timestamp));
|
onFrameSelected(eventTimeline[index], getSeekSeconds(eventTimeline[index].timestamp + annotationOffset));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!eventTimeline || !config) {
|
if (!eventTimeline || !config) {
|
||||||
|
|||||||
@ -206,7 +206,7 @@ export default function Events({ path, ...props }) {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setDownloadEvent((_prev) => ({
|
setDownloadEvent((_prev) => ({
|
||||||
id: event.id,
|
id: event.id,
|
||||||
box: event.box,
|
box: event?.data?.box || event.box,
|
||||||
label: event.label,
|
label: event.label,
|
||||||
has_clip: event.has_clip,
|
has_clip: event.has_clip,
|
||||||
has_snapshot: event.has_snapshot,
|
has_snapshot: event.has_snapshot,
|
||||||
@ -599,7 +599,7 @@ export default function Events({ path, ...props }) {
|
|||||||
{event.sub_label
|
{event.sub_label
|
||||||
? `${event.label.replaceAll('_', ' ')}: ${event.sub_label.replaceAll('_', ' ')}`
|
? `${event.label.replaceAll('_', ' ')}: ${event.sub_label.replaceAll('_', ' ')}`
|
||||||
: event.label.replaceAll('_', ' ')}
|
: event.label.replaceAll('_', ' ')}
|
||||||
({(event.top_score * 100).toFixed(0)}%)
|
({((event?.data?.top_score || event.top_score) * 100).toFixed(0)}%)
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm flex">
|
<div className="text-sm flex">
|
||||||
<Clock className="h-5 w-5 mr-2 inline" />
|
<Clock className="h-5 w-5 mr-2 inline" />
|
||||||
@ -638,7 +638,9 @@ export default function Events({ path, ...props }) {
|
|||||||
<Button
|
<Button
|
||||||
color="gray"
|
color="gray"
|
||||||
disabled={uploading.includes(event.id)}
|
disabled={uploading.includes(event.id)}
|
||||||
onClick={(e) => showSubmitToPlus(event.id, event.label, event.box, e)}
|
onClick={(e) =>
|
||||||
|
showSubmitToPlus(event.id, event.label, event?.data?.box || event.box, e)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{uploading.includes(event.id) ? 'Uploading...' : 'Send to Frigate+'}
|
{uploading.includes(event.id) ? 'Uploading...' : 'Send to Frigate+'}
|
||||||
</Button>
|
</Button>
|
||||||
@ -680,7 +682,9 @@ export default function Events({ path, ...props }) {
|
|||||||
<div>
|
<div>
|
||||||
<TimelineSummary
|
<TimelineSummary
|
||||||
event={event}
|
event={event}
|
||||||
onFrameSelected={(frame, seekSeconds) => onEventFrameSelected(event, frame, seekSeconds)}
|
onFrameSelected={(frame, seekSeconds) =>
|
||||||
|
onEventFrameSelected(event, frame, seekSeconds)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<VideoPlayer
|
<VideoPlayer
|
||||||
@ -738,7 +742,9 @@ export default function Events({ path, ...props }) {
|
|||||||
? `${apiHost}/api/events/${event.id}/snapshot.jpg`
|
? `${apiHost}/api/events/${event.id}/snapshot.jpg`
|
||||||
: `${apiHost}/api/events/${event.id}/thumbnail.jpg`
|
: `${apiHost}/api/events/${event.id}/thumbnail.jpg`
|
||||||
}
|
}
|
||||||
alt={`${event.label} at ${(event.top_score * 100).toFixed(0)}% confidence`}
|
alt={`${event.label} at ${((event?.data?.top_score || event.top_score) * 100).toFixed(
|
||||||
|
0
|
||||||
|
)}% confidence`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@ -345,6 +345,33 @@ export default function System() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Heading size="lg">Other Processes</Heading>
|
||||||
|
<div data-testid="cameras" className="grid grid-cols-1 3xl:grid-cols-3 md:grid-cols-2 gap-4">
|
||||||
|
{['go2rtc', 'recording'].map((process) => (
|
||||||
|
<div key={process} className="dark:bg-gray-800 shadow-md hover:shadow-lg rounded-lg transition-shadow">
|
||||||
|
<div className="capitalize text-lg flex justify-between p-4">
|
||||||
|
<div className="text-lg flex justify-between">{process}</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-2">
|
||||||
|
<Table className="w-full">
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th>CPU %</Th>
|
||||||
|
<Th>Memory %</Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
<Tr key="ffmpeg" index="0">
|
||||||
|
<Td>{cpu_usages[process]?.['cpu'] || '- '}%</Td>
|
||||||
|
<Td>{cpu_usages[process]?.['mem'] || '- '}%</Td>
|
||||||
|
</Tr>
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<p>System stats update automatically every {config.mqtt.stats_interval} seconds.</p>
|
<p>System stats update automatically every {config.mqtt.stats_interval} seconds.</p>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user