From 83006eeb65289d45b9e55678373e63a51c4f32ae Mon Sep 17 00:00:00 2001 From: Alin Balutoiu Date: Thu, 27 Apr 2023 02:29:01 +0200 Subject: [PATCH 01/19] Add option to have cameras sorted inside the Birdseye stream (#5450) * Add option to sort cameras inside Birdseye * Make default order to be sorted alphabetically * Add docs for sorting cameras * Update index.md for cameras * Remove irelevant comments --- docs/docs/configuration/birdseye.md | 22 ++++++++++++++++++++++ docs/docs/configuration/index.md | 6 ++++++ frigate/config.py | 1 + frigate/output.py | 25 ++++++++++++++++++++++++- 4 files changed, 53 insertions(+), 1 deletion(-) diff --git a/docs/docs/configuration/birdseye.md b/docs/docs/configuration/birdseye.md index 0b54a5f5f..6471bf4e3 100644 --- a/docs/docs/configuration/birdseye.md +++ b/docs/docs/configuration/birdseye.md @@ -33,3 +33,25 @@ cameras: birdseye: enabled: False ``` + +### Sorting cameras in the Birdseye view + +It is possible to override the order of cameras that are being shown in the Birdseye view. +The order needs to be set at the camera level. + +```yaml +# Include all cameras by default in Birdseye view +birdseye: + enabled: True + mode: continuous + +cameras: + front: + birdseye: + order: 1 + back: + birdseye: + order: 2 +``` + +*Note*: Cameras are sorted by default using their name to ensure a constant view inside Birdseye. diff --git a/docs/docs/configuration/index.md b/docs/docs/configuration/index.md index 93a266b56..5a1284ebb 100644 --- a/docs/docs/configuration/index.md +++ b/docs/docs/configuration/index.md @@ -518,6 +518,12 @@ cameras: # Optional: password for login. password: admin + # Optional: Configuration for how to sort the cameras in the Birdseye view. + birdseye: + # Optional: Adjust sort order of cameras in the Birdseye view. Larger numbers come later (default: shown below) + # By default the cameras are sorted alphabetically. + order: 0 + # Optional ui: # Optional: Set the default live mode for cameras in the UI (default: shown below) diff --git a/frigate/config.py b/frigate/config.py index b7d965282..f3e760b6a 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -396,6 +396,7 @@ class BirdseyeConfig(FrigateBaseModel): # uses BaseModel because some global attributes are not available at the camera level class BirdseyeCameraConfig(BaseModel): enabled: bool = Field(default=True, title="Enable birdseye view for camera.") + order: int = Field(default=0, title="Position of the camera in the birdseye view.") mode: BirdseyeModeEnum = Field( default=BirdseyeModeEnum.objects, title="Tracking mode for camera." ) diff --git a/frigate/output.py b/frigate/output.py index b59d56a62..79a081837 100644 --- a/frigate/output.py +++ b/frigate/output.py @@ -4,6 +4,7 @@ import logging import math import multiprocessing as mp import os +import operator import queue import signal import subprocess as sp @@ -292,8 +293,16 @@ class BirdsEyeFrameManager: # calculate layout dimensions layout_dim = math.ceil(math.sqrt(len(active_cameras))) + # check if we need to reset the layout because there are new cameras to add + reset_layout = ( + True if len(active_cameras.difference(self.active_cameras)) > 0 else False + ) + # reset the layout if it needs to be different - if layout_dim != self.layout_dim: + if layout_dim != self.layout_dim or reset_layout: + if reset_layout: + logger.debug(f"Added new cameras, resetting layout...") + logger.debug(f"Changing layout size from {self.layout_dim} to {layout_dim}") self.layout_dim = layout_dim @@ -327,6 +336,20 @@ class BirdsEyeFrameManager: self.active_cameras = active_cameras + # this also converts added_cameras from a set to a list since we need + # to pop elements in order + added_cameras = sorted( + added_cameras, + # sort cameras by order and by name if the order is the same + key=lambda added_camera: ( + self.config.cameras[added_camera].birdseye.order, + added_camera, + ), + # we're popping out elements from the end, so this needs to be reverse + # as we want the last element to be the first + reverse=True, + ) + # update each position in the layout for position, camera in enumerate(self.camera_layout, start=0): # if this camera was removed, replace it or clear it From b48f6d750fc577af24562c6c47c73b545a4456bc Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Fri, 28 Apr 2023 05:02:06 -0600 Subject: [PATCH 02/19] Add annotation support (#6288) --- frigate/config.py | 3 +++ web/src/components/TimelineSummary.jsx | 12 ++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/frigate/config.py b/frigate/config.py index f3e760b6a..85685e8e9 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -269,6 +269,9 @@ class DetectConfig(FrigateBaseModel): default_factory=StationaryConfig, title="Stationary objects config.", ) + annotation_offset: int = Field( + default=0, title="Milliseconds to offset detect annotations by." + ) class FilterConfig(FrigateBaseModel): diff --git a/web/src/components/TimelineSummary.jsx b/web/src/components/TimelineSummary.jsx index 8e5af9654..7a16bb1aa 100644 --- a/web/src/components/TimelineSummary.jsx +++ b/web/src/components/TimelineSummary.jsx @@ -5,7 +5,7 @@ import { formatUnixTimestampToDateTime } from '../utils/dateUtil'; import PlayIcon from '../icons/Play'; import ExitIcon from '../icons/Exit'; import { Zone } from '../icons/Zone'; -import { useState } from 'preact/hooks'; +import { useMemo, useState } from 'preact/hooks'; import Button from './Button'; export default function TimelineSummary({ event, onFrameSelected }) { @@ -18,6 +18,14 @@ export default function TimelineSummary({ event, onFrameSelected }) { 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 recordingParams = { @@ -53,7 +61,7 @@ export default function TimelineSummary({ event, onFrameSelected }) { const onSelectMoment = async (index) => { setTimeIndex(index); - onFrameSelected(eventTimeline[index], getSeekSeconds(eventTimeline[index].timestamp)); + onFrameSelected(eventTimeline[index], getSeekSeconds(eventTimeline[index].timestamp + annotationOffset)); }; if (!eventTimeline || !config) { From 37360edbc165ff031bf4b21b922bc0e44d703264 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Fri, 28 Apr 2023 06:47:49 -0600 Subject: [PATCH 03/19] Show other system processes in system page (#6276) --- frigate/util.py | 9 ++++++++- web/src/routes/System.jsx | 27 +++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/frigate/util.py b/frigate/util.py index 12139bf99..b01234c1a 100755 --- a/frigate/util.py +++ b/frigate/util.py @@ -824,7 +824,14 @@ def get_cpu_stats() -> dict[str, dict]: else: mem_pct = stats[9] - usages[stats[0]] = { + idx = stats[0] + + if stats[-1] == "go2rtc": + idx = "go2rtc" + elif stats[-1] == "frigate.r+": + idx = "recording" + + usages[idx] = { "cpu": stats[8], "mem": mem_pct, } diff --git a/web/src/routes/System.jsx b/web/src/routes/System.jsx index 8a5f2c28f..0320e237b 100644 --- a/web/src/routes/System.jsx +++ b/web/src/routes/System.jsx @@ -345,6 +345,33 @@ export default function System() { )} + Other Processes +
+ {['go2rtc', 'recording'].map((process) => ( +
+
+
{process}
+
+
+ + + + + + + + + + + + + +
CPU %Memory %
{cpu_usages[process]?.['cpu'] || '- '}%{cpu_usages[process]?.['mem'] || '- '}%
+
+
+ ))} +
+

System stats update automatically every {config.mqtt.stats_interval} seconds.

)} From ca7790ff65c943f4f57585f5d9a03b699535af3b Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Sun, 30 Apr 2023 08:28:21 -0600 Subject: [PATCH 04/19] Cleanup timeline entries when relevant recording segments are removed (#6319) * Cleanup timeline entries when relevant recording segments are removed * Make timeline cleanup simpler * Formatting --- frigate/record/cleanup.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/frigate/record/cleanup.py b/frigate/record/cleanup.py index 74c7eadf2..d649a2d87 100644 --- a/frigate/record/cleanup.py +++ b/frigate/record/cleanup.py @@ -12,7 +12,7 @@ from multiprocessing.synchronize import Event as MpEvent from frigate.config import RetainModeEnum, FrigateConfig 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 logger = logging.getLogger(__name__) @@ -140,6 +140,15 @@ class RecordingCleanup(threading.Thread): Path(recording.path).unlink(missing_ok=True) deleted_recordings.add(recording.id) + # delete timeline entries relevant to this recording segment + Timeline.delete( + 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") # delete up to 100,000 at a time max_deletes = 100000 From ad52e238cebd9d8179cbe4cb4c5abc4c75fff572 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Sun, 30 Apr 2023 11:07:14 -0600 Subject: [PATCH 05/19] Refactor events to be more generic (#6320) * Organize event table to be more generalized * Add appropriate fields to data * Move tracked object logic to own function * Add source type to event queue * rename enum * Fix types that are used in webUI * remove redundant * Formatting * fix typing * Rename enum --- frigate/events.py | 199 +++++++++++++---------- frigate/http.py | 20 ++- frigate/models.py | 21 ++- frigate/object_processing.py | 21 ++- frigate/timeline.py | 11 +- migrations/015_event_refactor.py | 49 ++++++ web/src/components/RecordingPlaylist.jsx | 4 +- web/src/routes/Events.jsx | 16 +- 8 files changed, 221 insertions(+), 120 deletions(-) create mode 100644 migrations/015_event_refactor.py diff --git a/frigate/events.py b/frigate/events.py index 558cb2139..965651edb 100644 --- a/frigate/events.py +++ b/frigate/events.py @@ -3,6 +3,8 @@ import logging import os import queue import threading + +from enum import Enum from pathlib import Path from peewee import fn @@ -10,7 +12,6 @@ from peewee import fn from frigate.config import EventsConfig, FrigateConfig from frigate.const import CLIPS_DIR from frigate.models import Event -from frigate.timeline import TimelineSourceEnum from frigate.types import CameraMetricsTypes from frigate.util import to_relative_box @@ -21,6 +22,12 @@ from typing import Dict 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: """If current_event has updated fields and (clip or 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(): 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: continue @@ -75,100 +84,19 @@ class EventProcessor(threading.Thread): self.timeline_queue.put( ( camera, - TimelineSourceEnum.tracked_object, + source_type, event_type, self.events_in_process.get(event_data["id"]), event_data, ) ) - # if this is the first message, just store it and continue, its not time to insert it in the db - if event_type == "start": - self.events_in_process[event_data["id"]] = event_data - continue + if source_type == EventTypeEnum.tracked_object: + if event_type == "start": + self.events_in_process[event_data["id"]] = event_data + continue - 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.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)) + self.handle_object_detection(event_type, camera, event_data) # set an end_time on events without an end_time before exiting Event.update(end_time=datetime.datetime.now().timestamp()).where( @@ -176,6 +104,99 @@ class EventProcessor(threading.Thread): ).execute() 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): def __init__(self, config: FrigateConfig, stop_event: MpEvent): diff --git a/frigate/http.py b/frigate/http.py index cd2a0c523..198b2be16 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -44,7 +44,6 @@ from frigate.util import ( restart_frigate, vainfo_hwaccel, get_tz_modifiers, - to_relative_box, ) from frigate.storage import StorageMaintainer from frigate.version import VERSION @@ -196,7 +195,7 @@ def send_to_plus(id): return make_response(jsonify({"success": False, "message": message}), 404) # 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 if event.end_time is None: @@ -252,8 +251,8 @@ def send_to_plus(id): event.save() if not include_annotation is None: - region = event.region - box = event.box + region = event.data["region"] + box = event.data["box"] try: current_app.plus_api.add_annotation( @@ -294,7 +293,7 @@ def false_positive(id): return make_response(jsonify({"success": False, "message": message}), 404) # 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" logger.error(message) 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 event = Event.get(Event.id == id) - region = event.region - box = event.box + region = event.data["region"] + box = event.data["box"] # 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: current_app.plus_api.add_false_positive( @@ -756,6 +759,7 @@ def events(): Event.top_score, Event.false_positive, Event.box, + Event.data, ] if camera != "all": diff --git a/frigate/models.py b/frigate/models.py index cc1a29f31..3770121cc 100644 --- a/frigate/models.py +++ b/frigate/models.py @@ -18,22 +18,33 @@ class Event(Model): # type: ignore[misc] camera = CharField(index=True, max_length=20) start_time = DateTimeField() end_time = DateTimeField() - top_score = FloatField() - score = FloatField() + top_score = ( + 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() zones = JSONField() thumbnail = TextField() has_clip = BooleanField(default=True) has_snapshot = BooleanField(default=True) - region = JSONField() - box = JSONField() - area = IntegerField() + region = ( + JSONField() + ) # 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) ratio = FloatField(default=1.0) plus_id = CharField(max_length=30) model_hash = CharField(max_length=32) detector_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] diff --git a/frigate/object_processing.py b/frigate/object_processing.py index db2745309..cc1667ffb 100644 --- a/frigate/object_processing.py +++ b/frigate/object_processing.py @@ -21,6 +21,7 @@ from frigate.config import ( FrigateConfig, ) from frigate.const import CLIPS_DIR +from frigate.events import EventTypeEnum from frigate.util import ( SharedMemoryFrameManager, calculate_region, @@ -656,7 +657,9 @@ class TrackedObjectProcessor(threading.Thread): self.last_motion_detected: dict[str, float] = {} 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): 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) obj.previous = after 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): @@ -722,7 +730,14 @@ class TrackedObjectProcessor(threading.Thread): } 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): mqtt_config: MqttConfig = self.config.cameras[camera].mqtt diff --git a/frigate/timeline.py b/frigate/timeline.py index c351e3e68..6b969bc0e 100644 --- a/frigate/timeline.py +++ b/frigate/timeline.py @@ -4,9 +4,8 @@ import logging import threading import queue -from enum import Enum - from frigate.config import FrigateConfig +from frigate.events import EventTypeEnum from frigate.models import Timeline from multiprocessing.queues import Queue @@ -17,12 +16,6 @@ from frigate.util import to_relative_box logger = logging.getLogger(__name__) -class TimelineSourceEnum(str, Enum): - # api = "api" - # audio = "audio" - tracked_object = "tracked_object" - - class TimelineProcessor(threading.Thread): """Handle timeline queue and update DB.""" @@ -51,7 +44,7 @@ class TimelineProcessor(threading.Thread): except queue.Empty: continue - if input_type == TimelineSourceEnum.tracked_object: + if input_type == EventTypeEnum.tracked_object: self.handle_object_detection( camera, event_type, prev_event_data, event_data ) diff --git a/migrations/015_event_refactor.py b/migrations/015_event_refactor.py new file mode 100644 index 000000000..d8a8a387c --- /dev/null +++ b/migrations/015_event_refactor.py @@ -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 diff --git a/web/src/components/RecordingPlaylist.jsx b/web/src/components/RecordingPlaylist.jsx index 4d6f93842..4f6996afc 100644 --- a/web/src/components/RecordingPlaylist.jsx +++ b/web/src/components/RecordingPlaylist.jsx @@ -163,7 +163,9 @@ export function EventCard({ camera, event }) {
Start: {format(start, 'HH:mm:ss')}
Duration: {duration}
-
{(event.top_score * 100).toFixed(1)}%
+
+ {((event?.data?.top_score || event.top_score) * 100).toFixed(1)}% +
diff --git a/web/src/routes/Events.jsx b/web/src/routes/Events.jsx index af0af5b67..30e3507f6 100644 --- a/web/src/routes/Events.jsx +++ b/web/src/routes/Events.jsx @@ -206,7 +206,7 @@ export default function Events({ path, ...props }) { e.stopPropagation(); setDownloadEvent((_prev) => ({ id: event.id, - box: event.box, + box: event?.data?.box || event.box, label: event.label, has_clip: event.has_clip, has_snapshot: event.has_snapshot, @@ -599,7 +599,7 @@ export default function Events({ path, ...props }) { {event.sub_label ? `${event.label.replaceAll('_', ' ')}: ${event.sub_label.replaceAll('_', ' ')}` : event.label.replaceAll('_', ' ')} - ({(event.top_score * 100).toFixed(0)}%) + ({((event?.data?.top_score || event.top_score) * 100).toFixed(0)}%)
@@ -638,7 +638,9 @@ export default function Events({ path, ...props }) { @@ -680,7 +682,9 @@ export default function Events({ path, ...props }) {
onEventFrameSelected(event, frame, seekSeconds)} + onFrameSelected={(frame, seekSeconds) => + onEventFrameSelected(event, frame, seekSeconds) + } />
) : null} From 9bf98f908d5c5b1c9b4a91075a357d095fdae8ce Mon Sep 17 00:00:00 2001 From: Blake Blackshear Date: Sun, 30 Apr 2023 13:32:36 -0500 Subject: [PATCH 06/19] add plus integration for models (#6328) --- docs/docs/integrations/plus.md | 30 +++++++-- frigate/app.py | 15 +++-- frigate/config.py | 8 ++- frigate/const.py | 1 + frigate/detectors/detector_config.py | 44 +++++++++++++ frigate/http.py | 5 ++ frigate/plus.py | 23 ++++++- frigate/test/test_config.py | 97 ++++++++++++++-------------- frigate/test/test_http.py | 6 +- 9 files changed, 166 insertions(+), 63 deletions(-) diff --git a/docs/docs/integrations/plus.md b/docs/docs/integrations/plus.md index 4bab29ef3..7ffce4941 100644 --- a/docs/docs/integrations/plus.md +++ b/docs/docs/integrations/plus.md @@ -5,13 +5,11 @@ title: Frigate+ :::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. - -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. +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. ## 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. @@ -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. ![Annotate](/img/annotate.png) + +## 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 +``` diff --git a/frigate/app.py b/frigate/app.py index 095a51e36..a07a4465f 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -18,7 +18,14 @@ from frigate.comms.dispatcher import Communicator, Dispatcher from frigate.comms.mqtt import MqttClient from frigate.comms.ws import WebSocketClient 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.events import EventCleanup, EventProcessor from frigate.http import create_app @@ -57,7 +64,7 @@ class FrigateApp: os.environ[key] = value 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): logger.info(f"Creating directory: {d}") os.makedirs(d) @@ -81,7 +88,7 @@ class FrigateApp: config_file = config_file_yaml 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(): # create camera_metrics @@ -379,6 +386,7 @@ class FrigateApp: self.init_logger() logger.info(f"Starting Frigate ({VERSION})") try: + self.ensure_dirs() try: self.init_config() except Exception as e: @@ -399,7 +407,6 @@ class FrigateApp: self.log_process.terminate() sys.exit(1) self.set_environment_vars() - self.ensure_dirs() self.set_log_levels() self.init_queues() self.init_database() diff --git a/frigate/config.py b/frigate/config.py index 85685e8e9..dc66e7896 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -19,6 +19,7 @@ from frigate.const import ( YAML_EXT, ) from frigate.detectors.detector_config import BaseDetectorConfig +from frigate.plus import PlusApi from frigate.util import ( create_mask, deep_merge, @@ -906,8 +907,7 @@ class FrigateConfig(FrigateBaseModel): title="Global timestamp style configuration.", ) - @property - def runtime_config(self) -> FrigateConfig: + def runtime_config(self, plus_api: PlusApi = None) -> FrigateConfig: """Merge camera config with globals.""" config = self.copy(deep=True) @@ -1031,6 +1031,7 @@ class FrigateConfig(FrigateBaseModel): enabled_labels.update(camera.objects.track) config.model.create_colormap(sorted(enabled_labels)) + config.model.check_and_load_plus_model(plus_api) for key, detector in config.detectors.items(): detector_config: DetectorConfig = parse_obj_as(DetectorConfig, detector) @@ -1063,6 +1064,9 @@ class FrigateConfig(FrigateBaseModel): merged_model["path"] = "/edgetpu_model.tflite" 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() config.detectors[key] = detector_config diff --git a/frigate/const.py b/frigate/const.py index 8e1e42bb9..cfcdc2a53 100644 --- a/frigate/const.py +++ b/frigate/const.py @@ -1,5 +1,6 @@ CONFIG_DIR = "/config" DEFAULT_DB_PATH = f"{CONFIG_DIR}/frigate.db" +MODEL_CACHE_DIR = f"{CONFIG_DIR}/model_cache" BASE_DIR = "/media/frigate" CLIPS_DIR = f"{BASE_DIR}/clips" RECORD_DIR = f"{BASE_DIR}/recordings" diff --git a/frigate/detectors/detector_config.py b/frigate/detectors/detector_config.py index 46f92e6ab..16ffe5fd4 100644 --- a/frigate/detectors/detector_config.py +++ b/frigate/detectors/detector_config.py @@ -1,11 +1,16 @@ import hashlib +import json import logging from enum import Enum +import os from typing import Dict, List, Optional, Tuple, Union, Literal + +import requests import matplotlib.pyplot as plt from pydantic import BaseModel, Extra, Field, validator from pydantic.fields import PrivateAttr +from frigate.plus import PlusApi from frigate.util import load_labels @@ -73,6 +78,45 @@ class ModelConfig(BaseModel): } 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: with open(self.path, "rb") as f: file_hash = hashlib.md5() diff --git a/frigate/http.py b/frigate/http.py index 198b2be16..706487e8f 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -866,6 +866,11 @@ def config(): 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.detectors[ + detector + ].model.merged_labelmap + return jsonify(config) diff --git a/frigate/plus.py b/frigate/plus.py index 4b59b961e..7ba67a62d 100644 --- a/frigate/plus.py +++ b/frigate/plus.py @@ -3,7 +3,7 @@ import json import logging import os import re -from typing import List +from typing import Any, Dict, List import requests from frigate.const import PLUS_ENV_VAR, PLUS_API_HOST from requests.models import Response @@ -187,3 +187,24 @@ class PlusApi: if not r.ok: 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() diff --git a/frigate/test/test_config.py b/frigate/test/test_config.py index b978453c1..a40093139 100644 --- a/frigate/test/test_config.py +++ b/frigate/test/test_config.py @@ -34,7 +34,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**self.minimal) 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 runtime_config.detectors["cpu"].type == DetectorTypeEnum.cpu assert runtime_config.detectors["cpu"].model.width == 320 @@ -59,7 +59,7 @@ class TestConfig(unittest.TestCase): } 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 "edgetpu" in runtime_config.detectors.keys() @@ -125,7 +125,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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 def test_override_birdseye(self): @@ -151,7 +151,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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 runtime_config.cameras["back"].birdseye.mode is BirdseyeModeEnum.motion @@ -177,7 +177,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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 def test_inherit_birdseye(self): @@ -202,7 +202,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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.mode is BirdseyeModeEnum.continuous @@ -231,7 +231,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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 def test_default_object_filters(self): @@ -256,7 +256,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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 def test_inherit_object_filters(self): @@ -284,7 +284,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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 runtime_config.cameras["back"].objects.filters["dog"].threshold == 0.7 @@ -313,7 +313,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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 runtime_config.cameras["back"].objects.filters["dog"].threshold == 0.7 @@ -343,7 +343,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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"] assert "dog" in back_camera.objects.filters assert len(back_camera.objects.filters["dog"].raw_mask) == 2 @@ -374,7 +374,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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"] def test_ffmpeg_params_global(self): @@ -403,7 +403,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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"] def test_ffmpeg_params_camera(self): @@ -433,7 +433,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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 "test" not in runtime_config.cameras["back"].ffmpeg_cmds[0]["cmd"] @@ -468,7 +468,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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 "test" in runtime_config.cameras["back"].ffmpeg_cmds[0]["cmd"] assert "test2" not in runtime_config.cameras["back"].ffmpeg_cmds[0]["cmd"] @@ -498,7 +498,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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"].record.events.retain.objects["person"] == 30 ) @@ -576,7 +576,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) assert config == frigate_config.dict(exclude_unset=True) - runtime_config = frigate_config.runtime_config + runtime_config = frigate_config.runtime_config() assert isinstance( runtime_config.cameras["back"].zones["test"].contour, np.ndarray ) @@ -608,7 +608,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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"] assert back_camera.record.events.objects is None assert back_camera.record.events.retain.objects["person"] == 30 @@ -639,7 +639,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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 assert len(ffmpeg_cmds) == 1 assert not "clips" in ffmpeg_cmds[0]["roles"] @@ -670,7 +670,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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 def test_motion_frame_height_wont_go_below_120(self): @@ -698,7 +698,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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 def test_motion_contour_area_dynamic(self): @@ -726,7 +726,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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 def test_merge_labelmap(self): @@ -755,7 +755,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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" def test_default_labelmap_empty(self): @@ -783,7 +783,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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" def test_default_labelmap(self): @@ -812,7 +812,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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" def test_fails_on_invalid_role(self): @@ -871,7 +871,7 @@ class TestConfig(unittest.TestCase): } 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): config = { @@ -919,7 +919,7 @@ class TestConfig(unittest.TestCase): } frigate_config = FrigateConfig(**config) - runtime_config = frigate_config.runtime_config + runtime_config = frigate_config.runtime_config() def test_global_detect(self): config = { @@ -946,7 +946,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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.height == 1080 @@ -969,7 +969,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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.height == 720 @@ -998,7 +998,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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.height == 1080 assert runtime_config.cameras["back"].detect.width == 1920 @@ -1026,7 +1026,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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.height == 100 @@ -1049,7 +1049,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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.quality == 70 @@ -1077,7 +1077,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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.height == 150 assert runtime_config.cameras["back"].snapshots.enabled @@ -1101,7 +1101,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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 def test_default_not_rtmp(self): @@ -1123,7 +1123,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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 def test_global_rtmp_merge(self): @@ -1149,7 +1149,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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 def test_global_rtmp_default(self): @@ -1175,7 +1175,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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 def test_global_jsmpeg(self): @@ -1198,7 +1198,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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 def test_default_live(self): @@ -1220,7 +1220,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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 def test_global_live_merge(self): @@ -1246,7 +1246,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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.height == 480 @@ -1270,7 +1270,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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" def test_default_timestamp_style(self): @@ -1292,7 +1292,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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" def test_global_timestamp_style_merge(self): @@ -1317,7 +1317,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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.thickness == 4 @@ -1341,7 +1341,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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 def test_fails_on_bad_camera_name(self): @@ -1365,7 +1365,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) self.assertRaises( - ValidationError, lambda: frigate_config.runtime_config.cameras + ValidationError, lambda: frigate_config.runtime_config().cameras ) def test_fails_on_bad_segment_time(self): @@ -1392,7 +1392,8 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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): @@ -1421,7 +1422,7 @@ class TestConfig(unittest.TestCase): 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): raw_config = """ @@ -1465,7 +1466,7 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) 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 runtime_config.cameras["back"].objects.filters["dog"].min_ratio == 0.2 assert runtime_config.cameras["back"].objects.filters["dog"].max_ratio == 10.1 diff --git a/frigate/test/test_http.py b/frigate/test/test_http.py index bc08ec010..2c0e58df2 100644 --- a/frigate/test/test_http.py +++ b/frigate/test/test_http.py @@ -292,7 +292,7 @@ class TestHttp(unittest.TestCase): def test_config(self): app = create_app( - FrigateConfig(**self.minimal_config).runtime_config, + FrigateConfig(**self.minimal_config).runtime_config(), self.db, None, None, @@ -308,7 +308,7 @@ class TestHttp(unittest.TestCase): def test_recordings(self): app = create_app( - FrigateConfig(**self.minimal_config).runtime_config, + FrigateConfig(**self.minimal_config).runtime_config(), self.db, None, None, @@ -327,7 +327,7 @@ class TestHttp(unittest.TestCase): @patch("frigate.http.stats_snapshot") def test_stats(self, mock_stats): app = create_app( - FrigateConfig(**self.minimal_config).runtime_config, + FrigateConfig(**self.minimal_config).runtime_config(), self.db, None, None, From 19890310fea8ce04b1b5ab2581ac1a6d3f9772c5 Mon Sep 17 00:00:00 2001 From: Blake Blackshear Date: Sun, 30 Apr 2023 14:58:39 -0500 Subject: [PATCH 07/19] Bug fixes (#6332) * fix timeline delete * fix labelmap in config response * create cache dirs if needed --- frigate/http.py | 6 ++-- frigate/record/cleanup.py | 2 +- frigate/test/test_config.py | 67 +++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 4 deletions(-) diff --git a/frigate/http.py b/frigate/http.py index 706487e8f..66aaff368 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -867,9 +867,9 @@ def config(): 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.detectors[ - detector - ].model.merged_labelmap + detector_config["model"][ + "labelmap" + ] = current_app.frigate_config.model.merged_labelmap return jsonify(config) diff --git a/frigate/record/cleanup.py b/frigate/record/cleanup.py index d649a2d87..75784133a 100644 --- a/frigate/record/cleanup.py +++ b/frigate/record/cleanup.py @@ -141,7 +141,7 @@ class RecordingCleanup(threading.Thread): deleted_recordings.add(recording.id) # delete timeline entries relevant to this recording segment - Timeline.delete( + Timeline.delete().where( Timeline.timestamp.between( recording.start_time, recording.end_time ), diff --git a/frigate/test/test_config.py b/frigate/test/test_config.py index a40093139..7cd5e2217 100644 --- a/frigate/test/test_config.py +++ b/frigate/test/test_config.py @@ -1,3 +1,5 @@ +import json +import os import unittest import numpy as np from pydantic import ValidationError @@ -6,7 +8,9 @@ from frigate.config import ( BirdseyeModeEnum, FrigateConfig, ) +from frigate.const import MODEL_CACHE_DIR from frigate.detectors import DetectorTypeEnum +from frigate.plus import PlusApi from frigate.util import deep_merge, load_config_with_no_duplicates @@ -30,6 +34,35 @@ 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): frigate_config = FrigateConfig(**self.minimal) assert self.minimal == frigate_config.dict(exclude_unset=True) @@ -815,6 +848,40 @@ class TestConfig(unittest.TestCase): runtime_config = frigate_config.runtime_config() 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): config = { "mqtt": {"host": "mqtt"}, From b38c9e82e2d51c5c28fad1b861b5ea8b3c6d5e24 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Tue, 2 May 2023 05:22:35 +0300 Subject: [PATCH 08/19] Replace subprocess usage with os module for better performance and maintainability (#6298) * avoid executing external tools by using Python's built-in os module to interact with the filesystem directly * Refactor recording cleanup script to use os module instead of subprocess * black format util.py * Ooooops * Refactor get_cpu_stats() to properly identify recording process --- frigate/record/cleanup.py | 26 ++++----- frigate/util.py | 112 ++++++++++++++++++-------------------- 2 files changed, 65 insertions(+), 73 deletions(-) diff --git a/frigate/record/cleanup.py b/frigate/record/cleanup.py index 75784133a..605979ee4 100644 --- a/frigate/record/cleanup.py +++ b/frigate/record/cleanup.py @@ -3,7 +3,7 @@ import datetime import itertools import logging -import subprocess as sp +import os import threading from pathlib import Path @@ -192,12 +192,14 @@ class RecordingCleanup(threading.Thread): return logger.debug(f"Oldest recording in the db: {oldest_timestamp}") - process = sp.run( - ["find", RECORD_DIR, "-type", "f", "!", "-newermt", f"@{oldest_timestamp}"], - capture_output=True, - text=True, - ) - files_to_check = process.stdout.splitlines() + + files_to_check = [] + + for root, _, files in os.walk(RECORD_DIR): + for file in files: + file_path = os.path.join(root, file) + if os.path.getmtime(file_path) < oldest_timestamp: + files_to_check.append(file_path) for f in files_to_check: p = Path(f) @@ -216,12 +218,10 @@ class RecordingCleanup(threading.Thread): recordings: Recordings = Recordings.select() # get all recordings files on disk - process = sp.run( - ["find", RECORD_DIR, "-type", "f"], - capture_output=True, - text=True, - ) - files_on_disk = process.stdout.splitlines() + files_on_disk = [] + for root, _, files in os.walk(RECORD_DIR): + for file in files: + files_on_disk.append(os.path.join(root, file)) recordings_to_delete = [] for recording in recordings.objects().iterator(): diff --git a/frigate/util.py b/frigate/util.py index b01234c1a..b26e28c9f 100755 --- a/frigate/util.py +++ b/frigate/util.py @@ -9,6 +9,7 @@ import signal import traceback import urllib.parse import yaml +import os from abc import ABC, abstractmethod from collections import Counter @@ -740,55 +741,54 @@ def escape_special_characters(path: str) -> str: def get_cgroups_version() -> str: - """Determine what version of cgroups is enabled""" + """Determine what version of cgroups is enabled.""" - stat_command = ["stat", "-fc", "%T", "/sys/fs/cgroup"] + cgroup_path = "/sys/fs/cgroup" - p = sp.run( - stat_command, - encoding="ascii", - capture_output=True, - ) + if not os.path.ismount(cgroup_path): + logger.debug(f"{cgroup_path} is not a mount point.") + return "unknown" - if p.returncode == 0: - value: str = p.stdout.strip().lower() + try: + with open("/proc/mounts", "r") as f: + mounts = f.readlines() - if value == "cgroup2fs": - return "cgroup2" - elif value == "tmpfs": - return "cgroup" - else: - logger.debug( - f"Could not determine cgroups version: unhandled filesystem {value}" - ) - else: - logger.debug(f"Could not determine cgroups version: {p.stderr}") + for mount in mounts: + mount_info = mount.split() + if mount_info[1] == cgroup_path: + fs_type = mount_info[2] + if fs_type == "cgroup2fs" or fs_type == "cgroup2": + return "cgroup2" + elif fs_type == "tmpfs": + return "cgroup" + else: + logger.debug( + f"Could not determine cgroups version: unhandled filesystem {fs_type}" + ) + break + except Exception as e: + logger.debug(f"Could not determine cgroups version: {e}") return "unknown" def get_docker_memlimit_bytes() -> int: - """Get mem limit in bytes set in docker if present. Returns -1 if no limit detected""" + """Get mem limit in bytes set in docker if present. Returns -1 if no limit detected.""" # check running a supported cgroups version if get_cgroups_version() == "cgroup2": - memlimit_command = ["cat", "/sys/fs/cgroup/memory.max"] + memlimit_path = "/sys/fs/cgroup/memory.max" - p = sp.run( - memlimit_command, - encoding="ascii", - capture_output=True, - ) - - if p.returncode == 0: - value: str = p.stdout.strip() + try: + with open(memlimit_path, "r") as f: + value = f.read().strip() if value.isnumeric(): return int(value) elif value.lower() == "max": return -1 - else: - logger.debug(f"Unable to get docker memlimit: {p.stderr}") + except Exception as e: + logger.debug(f"Unable to get docker memlimit: {e}") return -1 @@ -796,49 +796,41 @@ def get_docker_memlimit_bytes() -> int: def get_cpu_stats() -> dict[str, dict]: """Get cpu usages for each process id""" usages = {} - # -n=2 runs to ensure extraneous values are not included - top_command = ["top", "-b", "-n", "2"] - docker_memlimit = get_docker_memlimit_bytes() / 1024 + total_mem = os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES") / 1024 - p = sp.run( - top_command, - encoding="ascii", - capture_output=True, - ) - - if p.returncode != 0: - logger.error(p.stderr) - return usages - else: - lines = p.stdout.split("\n") - - for line in lines: - stats = list(filter(lambda a: a != "", line.strip().split(" "))) + for pid in os.listdir("/proc"): + if pid.isdigit(): try: + with open(f"/proc/{pid}/stat", "r") as f: + stats = f.readline().split() + utime = int(stats[13]) + stime = int(stats[14]) + cpu_usage = round((utime + stime) / os.sysconf("SC_CLK_TCK")) + + with open(f"/proc/{pid}/statm", "r") as f: + mem_stats = f.readline().split() + mem_res = int(mem_stats[1]) * os.sysconf("SC_PAGE_SIZE") / 1024 + if docker_memlimit > 0: - mem_res = int(stats[5]) - mem_pct = str( - round((float(mem_res) / float(docker_memlimit)) * 100, 1) - ) + mem_pct = round((mem_res / docker_memlimit) * 100, 1) else: - mem_pct = stats[9] + mem_pct = round((mem_res / total_mem) * 100, 1) - idx = stats[0] - - if stats[-1] == "go2rtc": + idx = pid + if stats[1] == "(go2rtc)": idx = "go2rtc" - elif stats[-1] == "frigate.r+": + if stats[1].startswith("(frigate.r"): idx = "recording" usages[idx] = { - "cpu": stats[8], - "mem": mem_pct, + "cpu": str(round(cpu_usage, 2)), + "mem": f"{mem_pct}", } except: continue - return usages + return usages def get_amd_gpu_stats() -> dict[str, str]: From 2add675d4242f9c550583fca5a8e66d93e2910e0 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Fri, 5 May 2023 01:58:59 +0300 Subject: [PATCH 09/19] Refactor Process Stats and Bugfixes (#6344) * test refactor process stats * Update util.py * bugfix * black formatting * add missing processes field to StatsTrackingTypes class * fix python checks and tests... * use psutil for calcilate cpu utilization on get_cpu_stats * black...black...black... * add cpu average * calculate statiscts for logger process * add P-ID for other processes in System.jsx * Apply suggestions from code review Co-authored-by: Nicolas Mowen * make page beautiful again :) --------- Co-authored-by: Nicolas Mowen --- frigate/app.py | 13 +++++++- frigate/stats.py | 8 +++++ frigate/types.py | 1 + frigate/util.py | 62 +++++++++++++++++++++++---------------- web/src/routes/System.jsx | 14 ++++++--- 5 files changed, 67 insertions(+), 31 deletions(-) diff --git a/frigate/app.py b/frigate/app.py index a07a4465f..d23073d77 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -8,6 +8,7 @@ import signal import sys from typing import Optional from types import FrameType +import psutil import traceback from peewee_migrate import Router @@ -58,6 +59,7 @@ class FrigateApp: self.plus_api = PlusApi() self.camera_metrics: dict[str, CameraMetricsTypes] = {} self.record_metrics: dict[str, RecordMetricsTypes] = {} + self.processes: dict[str, int] = {} def set_environment_vars(self) -> None: for key, value in self.config.environment_vars.items(): @@ -77,6 +79,7 @@ class FrigateApp: ) self.log_process.daemon = True self.log_process.start() + self.processes["logger"] = self.log_process.pid or 0 root_configurer(self.log_queue) def init_config(self) -> None: @@ -171,6 +174,12 @@ class FrigateApp: migrate_db.close() + def init_go2rtc(self) -> None: + for proc in psutil.process_iter(["pid", "name"]): + if proc.info["name"] == "go2rtc": + logger.info(f"go2rtc process pid: {proc.info['pid']}") + self.processes["go2rtc"] = proc.info["pid"] + def init_recording_manager(self) -> None: recording_process = mp.Process( target=manage_recordings, @@ -180,6 +189,7 @@ class FrigateApp: recording_process.daemon = True self.recording_process = recording_process recording_process.start() + self.processes["recording"] = recording_process.pid or 0 logger.info(f"Recording process started: {recording_process.pid}") def bind_database(self) -> None: @@ -191,7 +201,7 @@ class FrigateApp: def init_stats(self) -> None: self.stats_tracking = stats_init( - self.config, self.camera_metrics, self.detectors + self.config, self.camera_metrics, self.detectors, self.processes ) def init_web_server(self) -> None: @@ -412,6 +422,7 @@ class FrigateApp: self.init_database() self.init_onvif() self.init_recording_manager() + self.init_go2rtc() self.bind_database() self.init_dispatcher() except Exception as e: diff --git a/frigate/stats.py b/frigate/stats.py index 1ceb5f30d..c9b31bcc1 100644 --- a/frigate/stats.py +++ b/frigate/stats.py @@ -46,6 +46,7 @@ def stats_init( config: FrigateConfig, camera_metrics: dict[str, CameraMetricsTypes], detectors: dict[str, ObjectDetectProcess], + processes: dict[str, int], ) -> StatsTrackingTypes: stats_tracking: StatsTrackingTypes = { "camera_metrics": camera_metrics, @@ -53,6 +54,7 @@ def stats_init( "started": int(time.time()), "latest_frigate_version": get_latest_version(config), "last_updated": int(time.time()), + "processes": processes, } return stats_tracking @@ -260,6 +262,12 @@ def stats_snapshot( "mount_type": get_fs_type(path), } + stats["processes"] = {} + for name, pid in stats_tracking["processes"].items(): + stats["processes"][name] = { + "pid": pid, + } + return stats diff --git a/frigate/types.py b/frigate/types.py index 9da4027c9..3cc401ebb 100644 --- a/frigate/types.py +++ b/frigate/types.py @@ -34,3 +34,4 @@ class StatsTrackingTypes(TypedDict): started: int latest_frigate_version: str last_updated: int + processes: dict[str, int] diff --git a/frigate/util.py b/frigate/util.py index b26e28c9f..51d619005 100755 --- a/frigate/util.py +++ b/frigate/util.py @@ -799,36 +799,46 @@ def get_cpu_stats() -> dict[str, dict]: docker_memlimit = get_docker_memlimit_bytes() / 1024 total_mem = os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES") / 1024 - for pid in os.listdir("/proc"): - if pid.isdigit(): - try: - with open(f"/proc/{pid}/stat", "r") as f: - stats = f.readline().split() - utime = int(stats[13]) - stime = int(stats[14]) - cpu_usage = round((utime + stime) / os.sysconf("SC_CLK_TCK")) + for process in psutil.process_iter(["pid", "name", "cpu_percent"]): + pid = process.info["pid"] + try: + cpu_percent = process.info["cpu_percent"] - with open(f"/proc/{pid}/statm", "r") as f: - mem_stats = f.readline().split() - mem_res = int(mem_stats[1]) * os.sysconf("SC_PAGE_SIZE") / 1024 + with open(f"/proc/{pid}/stat", "r") as f: + stats = f.readline().split() + utime = int(stats[13]) + stime = int(stats[14]) + starttime = int(stats[21]) - if docker_memlimit > 0: - mem_pct = round((mem_res / docker_memlimit) * 100, 1) - else: - mem_pct = round((mem_res / total_mem) * 100, 1) + with open("/proc/uptime") as f: + system_uptime_sec = int(float(f.read().split()[0])) - idx = pid - if stats[1] == "(go2rtc)": - idx = "go2rtc" - if stats[1].startswith("(frigate.r"): - idx = "recording" + clk_tck = os.sysconf(os.sysconf_names["SC_CLK_TCK"]) - usages[idx] = { - "cpu": str(round(cpu_usage, 2)), - "mem": f"{mem_pct}", - } - except: - continue + process_utime_sec = utime // clk_tck + process_stime_sec = stime // clk_tck + process_starttime_sec = starttime // clk_tck + + process_elapsed_sec = system_uptime_sec - process_starttime_sec + process_usage_sec = process_utime_sec + process_stime_sec + cpu_average_usage = process_usage_sec * 100 // process_elapsed_sec + + with open(f"/proc/{pid}/statm", "r") as f: + mem_stats = f.readline().split() + mem_res = int(mem_stats[1]) * os.sysconf("SC_PAGE_SIZE") / 1024 + + if docker_memlimit > 0: + mem_pct = round((mem_res / docker_memlimit) * 100, 1) + else: + mem_pct = round((mem_res / total_mem) * 100, 1) + + usages[pid] = { + "cpu": str(cpu_percent), + "cpu_average": str(round(cpu_average_usage, 2)), + "mem": f"{mem_pct}", + } + except: + continue return usages diff --git a/web/src/routes/System.jsx b/web/src/routes/System.jsx index 0320e237b..b6e4d3c9d 100644 --- a/web/src/routes/System.jsx +++ b/web/src/routes/System.jsx @@ -29,12 +29,14 @@ export default function System() { detectors, service = {}, detection_fps: _, + processes, ...cameras } = stats || initialStats || emptyObject; const detectorNames = Object.keys(detectors || emptyObject); const gpuNames = Object.keys(gpu_usages || emptyObject); const cameraNames = Object.keys(cameras || emptyObject); + const processesNames = Object.keys(processes || emptyObject); const onHandleFfprobe = async (camera, e) => { if (e) { @@ -347,7 +349,7 @@ export default function System() { Other Processes
- {['go2rtc', 'recording'].map((process) => ( + {processesNames.map((process) => (
{process}
@@ -356,14 +358,18 @@ export default function System() { + + - - - + + + + +
P-ID CPU %Avg CPU % Memory %
{cpu_usages[process]?.['cpu'] || '- '}%{cpu_usages[process]?.['mem'] || '- '}%
{processes[process]['pid'] || '- '}{cpu_usages[processes[process]['pid']]?.['cpu'] || '- '}%{cpu_usages[processes[process]['pid']]?.['cpu_average'] || '- '}%{cpu_usages[processes[process]['pid']]?.['mem'] || '- '}%
From 03b45c153b040824666fa2a0e2cb22a00aada33b Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Fri, 5 May 2023 01:59:44 +0300 Subject: [PATCH 10/19] Increase maximum event sub_label length to 100 characters (#6350) * Increase maximum sub label length to 100 characters and update corresponding fields in models and API * black format... --- docs/docs/integrations/api.md | 2 +- frigate/http.py | 4 ++-- frigate/models.py | 2 +- migrations/016_sublabel_increase.py | 12 ++++++++++++ 4 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 migrations/016_sublabel_increase.py diff --git a/docs/docs/integrations/api.md b/docs/docs/integrations/api.md index 9aec392a4..b5b80f3ea 100644 --- a/docs/docs/integrations/api.md +++ b/docs/docs/integrations/api.md @@ -213,7 +213,7 @@ Sets retain to false for the event id (event may be deleted quickly after removi ### `POST /api/events//sub_label` Set a sub label for an event. For example to update `person` -> `person's name` if they were recognized with facial recognition. -Sub labels must be 20 characters or shorter. +Sub labels must be 100 characters or shorter. ```json { diff --git a/frigate/http.py b/frigate/http.py index 66aaff368..94aa5c129 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -375,13 +375,13 @@ def set_sub_label(id): else: new_sub_label = None - if new_sub_label and len(new_sub_label) > 20: + if new_sub_label and len(new_sub_label) > 100: return make_response( jsonify( { "success": False, "message": new_sub_label - + " exceeds the 20 character limit for sub_label", + + " exceeds the 100 character limit for sub_label", } ), 400, diff --git a/frigate/models.py b/frigate/models.py index 3770121cc..0d5f1ab4a 100644 --- a/frigate/models.py +++ b/frigate/models.py @@ -14,7 +14,7 @@ from playhouse.sqlite_ext import JSONField class Event(Model): # type: ignore[misc] id = CharField(null=False, primary_key=True, max_length=30) label = CharField(index=True, max_length=20) - sub_label = CharField(max_length=20, null=True) + sub_label = CharField(max_length=100, null=True) camera = CharField(index=True, max_length=20) start_time = DateTimeField() end_time = DateTimeField() diff --git a/migrations/016_sublabel_increase.py b/migrations/016_sublabel_increase.py new file mode 100644 index 000000000..b46b9fc79 --- /dev/null +++ b/migrations/016_sublabel_increase.py @@ -0,0 +1,12 @@ +import peewee as pw +from playhouse.migrate import * +from playhouse.sqlite_ext import * +from frigate.models import Event + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.change_columns(Event, sub_label=pw.CharField(max_length=100, null=True)) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.change_columns(Event, sub_label=pw.CharField(max_length=20, null=True)) From 02f577347d3981a427d25407192a9526322e0f71 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Fri, 5 May 2023 02:00:18 +0300 Subject: [PATCH 11/19] Hide PTZ Controls in Birdseye when no cameras support it (#6353) * Refactor filter logic to exclude cameras with empty onvif host address * Refactor filter logic to exclude cameras with empty onvif host address-2 * Apply suggestions from code review Co-authored-by: Nicolas Mowen --------- Co-authored-by: Nicolas Mowen --- web/src/routes/Birdseye.jsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/web/src/routes/Birdseye.jsx b/web/src/routes/Birdseye.jsx index c23713bb9..c2742e14b 100644 --- a/web/src/routes/Birdseye.jsx +++ b/web/src/routes/Birdseye.jsx @@ -24,7 +24,7 @@ export default function Birdseye() { } return Object.entries(config.cameras) - .filter(([_, conf]) => conf.onvif?.host) + .filter(([_, conf]) => conf.onvif?.host && conf.onvif.host != '') .map(([_, camera]) => camera.name); }, [config]); @@ -37,7 +37,7 @@ export default function Birdseye() { if ('MediaSource' in window) { player = ( -
+
@@ -54,7 +54,7 @@ export default function Birdseye() { } else if (viewSource == 'webrtc' && config.birdseye.restream) { player = ( -
+
@@ -62,7 +62,7 @@ export default function Birdseye() { } else { player = ( -
+
@@ -94,7 +94,7 @@ export default function Birdseye() {
{player} - {ptzCameras && ( + {ptzCameras.length ? (
Control Panel {ptzCameras.map((camera) => ( @@ -104,7 +104,7 @@ export default function Birdseye() {
))}
- )} + ) : null}
); From ef50af03f26ca73171d41e62ed3a1f5f7ecb1113 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Thu, 4 May 2023 17:01:27 -0600 Subject: [PATCH 12/19] Add icon to explain in more detail the system stats / storage info (#6358) * Add info icon with additional info for each process. * Specify single CPU core * Include storage in tooltips --- web/src/icons/About.jsx | 20 +++++++++++++ web/src/routes/Storage.jsx | 40 +++++++++++++++++++++++--- web/src/routes/System.jsx | 59 ++++++++++++++++++++++++++++++++++---- 3 files changed, 110 insertions(+), 9 deletions(-) create mode 100644 web/src/icons/About.jsx diff --git a/web/src/icons/About.jsx b/web/src/icons/About.jsx new file mode 100644 index 000000000..444ad3227 --- /dev/null +++ b/web/src/icons/About.jsx @@ -0,0 +1,20 @@ +import { h } from 'preact'; +import { memo } from 'preact/compat'; + +export function About({ className = '' }) { + return ( + + + + ); +} + +export default memo(About); + diff --git a/web/src/routes/Storage.jsx b/web/src/routes/Storage.jsx index 4539df6f1..dd7f969c7 100644 --- a/web/src/routes/Storage.jsx +++ b/web/src/routes/Storage.jsx @@ -5,6 +5,8 @@ import { useWs } from '../api/ws'; import useSWR from 'swr'; import { Table, Tbody, Thead, Tr, Th, Td } from '../components/Table'; import Link from '../components/Link'; +import Button from '../components/Button'; +import { About } from '../icons/About'; const emptyObject = Object.freeze({}); @@ -66,9 +68,19 @@ export default function Storage() { Overview -
+
-
Data
+
+
Data
+ +
@@ -83,7 +95,17 @@ export default function Storage() {
-
Memory
+
+
Memory
+ +
@@ -110,7 +132,17 @@ export default function Storage() { - Cameras +
+ Cameras + +
{Object.entries(storage).map(([name, camera]) => (
diff --git a/web/src/routes/System.jsx b/web/src/routes/System.jsx index b6e4d3c9d..4eeaa71a7 100644 --- a/web/src/routes/System.jsx +++ b/web/src/routes/System.jsx @@ -11,6 +11,7 @@ import { useState } from 'preact/hooks'; import Dialog from '../components/Dialog'; import TimeAgo from '../components/TimeAgo'; import copy from 'copy-to-clipboard'; +import { About } from '../icons/About'; const emptyObject = Object.freeze({}); @@ -208,7 +209,19 @@ export default function System() {
) : ( - Detectors +
+ + Detectors + + +
{detectorNames.map((detector) => (
@@ -237,8 +250,20 @@ export default function System() { ))}
-
- GPUs +
+
+ + GPUs + + +
@@ -282,7 +307,19 @@ export default function System() {
)} - Cameras +
+ + Cameras + + +
{!cameras ? ( ) : ( @@ -347,7 +384,19 @@ export default function System() {
)} - Other Processes +
+ + Other Processes + + +
{processesNames.map((process) => (
From 0fcfcb85ab67c3d3c2408c0a7da940b4c85d6a26 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Fri, 5 May 2023 02:02:01 +0300 Subject: [PATCH 13/19] Implement NVML for NVIDIA GPU Stats (#6359) * nvml * black...black...black... * small fix for avoid errors on strange GPUs and old drivers * fix type errors * fix type errors * fix unittest process crash where the tests for tests?.. * it's impossible to mock low-level library * fix double % for other GPU types * remove space before gpu statistic values --- frigate/stats.py | 9 +++-- frigate/test/test_gpu_stats.py | 30 +++++++------- frigate/util.py | 73 +++++++++++++++++----------------- requirements-wheels.txt | 1 + 4 files changed, 58 insertions(+), 55 deletions(-) diff --git a/frigate/stats.py b/frigate/stats.py index c9b31bcc1..4287dab0c 100644 --- a/frigate/stats.py +++ b/frigate/stats.py @@ -153,9 +153,12 @@ async def set_gpu_stats( nvidia_usage = get_nvidia_gpu_stats() if nvidia_usage: - name = nvidia_usage["name"] - del nvidia_usage["name"] - stats[name] = nvidia_usage + for i in range(len(nvidia_usage)): + stats[nvidia_usage[i]["name"]] = { + "gpu": str(round(float(nvidia_usage[i]["gpu"]), 2)) + "%", + "mem": str(round(float(nvidia_usage[i]["mem"]), 2)) + "%", + } + else: stats["nvidia-gpu"] = {"gpu": -1, "mem": -1} hwaccel_errors.append(args) diff --git a/frigate/test/test_gpu_stats.py b/frigate/test/test_gpu_stats.py index d3f00ce77..5742a583d 100644 --- a/frigate/test/test_gpu_stats.py +++ b/frigate/test/test_gpu_stats.py @@ -17,20 +17,20 @@ class TestGpuStats(unittest.TestCase): process.stdout = self.amd_results sp.return_value = process amd_stats = get_amd_gpu_stats() - assert amd_stats == {"gpu": "4.17 %", "mem": "60.37 %"} + assert amd_stats == {"gpu": "4.17%", "mem": "60.37%"} - @patch("subprocess.run") - def test_nvidia_gpu_stats(self, sp): - process = MagicMock() - process.returncode = 0 - process.stdout = self.nvidia_results - sp.return_value = process - nvidia_stats = get_nvidia_gpu_stats() - assert nvidia_stats == { - "name": "NVIDIA GeForce RTX 3050", - "gpu": "42 %", - "mem": "61.5 %", - } + # @patch("subprocess.run") + # def test_nvidia_gpu_stats(self, sp): + # process = MagicMock() + # process.returncode = 0 + # process.stdout = self.nvidia_results + # sp.return_value = process + # nvidia_stats = get_nvidia_gpu_stats() + # assert nvidia_stats == { + # "name": "NVIDIA GeForce RTX 3050", + # "gpu": "42 %", + # "mem": "61.5 %", + # } @patch("subprocess.run") def test_intel_gpu_stats(self, sp): @@ -40,6 +40,6 @@ class TestGpuStats(unittest.TestCase): sp.return_value = process intel_stats = get_intel_gpu_stats() assert intel_stats == { - "gpu": "1.34 %", - "mem": "- %", + "gpu": "1.34%", + "mem": "-%", } diff --git a/frigate/util.py b/frigate/util.py index 51d619005..d98cee106 100755 --- a/frigate/util.py +++ b/frigate/util.py @@ -16,6 +16,7 @@ from collections import Counter from collections.abc import Mapping from multiprocessing import shared_memory from typing import Any, AnyStr, Optional, Tuple +import py3nvml.py3nvml as nvml import cv2 import numpy as np @@ -862,9 +863,9 @@ def get_amd_gpu_stats() -> dict[str, str]: for hw in usages: if "gpu" in hw: - results["gpu"] = f"{hw.strip().split(' ')[1].replace('%', '')} %" + results["gpu"] = f"{hw.strip().split(' ')[1].replace('%', '')}%" elif "vram" in hw: - results["mem"] = f"{hw.strip().split(' ')[1].replace('%', '')} %" + results["mem"] = f"{hw.strip().split(' ')[1].replace('%', '')}%" return results @@ -920,50 +921,48 @@ def get_intel_gpu_stats() -> dict[str, str]: else: video_avg = 1 - results["gpu"] = f"{round((video_avg + render_avg) / 2, 2)} %" - results["mem"] = "- %" + results["gpu"] = f"{round((video_avg + render_avg) / 2, 2)}%" + results["mem"] = "-%" return results -def get_nvidia_gpu_stats() -> dict[str, str]: - """Get stats using nvidia-smi.""" - nvidia_smi_command = [ - "nvidia-smi", - "--query-gpu=gpu_name,utilization.gpu,memory.used,memory.total", - "--format=csv", - ] +def try_get_info(f, h, default="N/A"): + try: + v = f(h) + except nvml.NVMLError_NotSupported: + v = default + return v - if ( - "CUDA_VISIBLE_DEVICES" in os.environ - and os.environ["CUDA_VISIBLE_DEVICES"].isdigit() - ): - nvidia_smi_command.extend(["--id", os.environ["CUDA_VISIBLE_DEVICES"]]) - elif ( - "NVIDIA_VISIBLE_DEVICES" in os.environ - and os.environ["NVIDIA_VISIBLE_DEVICES"].isdigit() - ): - nvidia_smi_command.extend(["--id", os.environ["NVIDIA_VISIBLE_DEVICES"]]) - p = sp.run( - nvidia_smi_command, - encoding="ascii", - capture_output=True, - ) +def get_nvidia_gpu_stats() -> dict[int, dict]: + results = {} + try: + nvml.nvmlInit() + deviceCount = nvml.nvmlDeviceGetCount() + for i in range(deviceCount): + handle = nvml.nvmlDeviceGetHandleByIndex(i) + meminfo = try_get_info(nvml.nvmlDeviceGetMemoryInfo, handle) + util = try_get_info(nvml.nvmlDeviceGetUtilizationRates, handle) + if util != "N/A": + gpu_util = util.gpu + else: + gpu_util = 0 - if p.returncode != 0: - logger.error(f"Unable to poll nvidia GPU stats: {p.stderr}") - return None - else: - usages = p.stdout.split("\n")[1].strip().split(",") - memory_percent = f"{round(float(usages[2].replace(' MiB', '').strip()) / float(usages[3].replace(' MiB', '').strip()) * 100, 1)} %" - results: dict[str, str] = { - "name": usages[0], - "gpu": usages[1].strip(), - "mem": memory_percent, - } + if meminfo != "N/A": + gpu_mem_util = meminfo.used / meminfo.total * 100 + else: + gpu_mem_util = -1 + results[i] = { + "name": nvml.nvmlDeviceGetName(handle), + "gpu": gpu_util, + "mem": gpu_mem_util, + } + except: return results + return results + def ffprobe_stream(path: str) -> sp.CompletedProcess: """Run ffprobe on stream.""" diff --git a/requirements-wheels.txt b/requirements-wheels.txt index e8e92408b..95d70077a 100644 --- a/requirements-wheels.txt +++ b/requirements-wheels.txt @@ -11,6 +11,7 @@ peewee == 3.15.* peewee_migrate == 1.7.* psutil == 5.9.* pydantic == 1.10.* +git+https://github.com/fbcotter/py3nvml#egg=py3nvml PyYAML == 6.0 pytz == 2023.3 tzlocal == 4.3 From 17b92aa65753f0abecd193e626a60b17cfa1ad35 Mon Sep 17 00:00:00 2001 From: Peeter N Date: Fri, 5 May 2023 02:02:48 +0300 Subject: [PATCH 14/19] Do not show the loader during recordings api call (#6366) * Do not show the loader during recordings api call Showing the loader during the recordings API call will rerender the recordings list and rerenders the video player * Fix defauting to 0 seekSeconds if recordings results are missings --- web/src/routes/Recording.jsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/web/src/routes/Recording.jsx b/web/src/routes/Recording.jsx index 9a40b674c..405549e8b 100644 --- a/web/src/routes/Recording.jsx +++ b/web/src/routes/Recording.jsx @@ -30,7 +30,7 @@ export default function Recording({ camera, date, hour = '00', minute = '00', se // calculates the seek seconds by adding up all the seconds in the segments prior to the playback time const seekSeconds = useMemo(() => { if (!recordings) { - return 0; + return undefined; } const currentUnix = getUnixTime(currentDate); @@ -103,6 +103,9 @@ export default function Recording({ camera, date, hour = '00', minute = '00', se }, [playlistIndex]); useEffect(() => { + if (seekSeconds === undefined) { + return; + } if (this.player) { // if the playlist has moved on to the next item, then reset if (this.player.playlist.currentItem() !== playlistIndex) { @@ -114,7 +117,7 @@ export default function Recording({ camera, date, hour = '00', minute = '00', se } }, [seekSeconds, playlistIndex]); - if (!recordingsSummary || !recordings || !config) { + if (!recordingsSummary || !config) { return ; } @@ -145,7 +148,9 @@ export default function Recording({ camera, date, hour = '00', minute = '00', se player.playlist(playlist); player.playlist.autoadvance(0); player.playlist.currentItem(playlistIndex); - player.currentTime(seekSeconds); + if (seekSeconds !== undefined) { + player.currentTime(seekSeconds); + } this.player = player; } }} From 0751358e5b926a7a1fa2db895ad62a03a5ddc65f Mon Sep 17 00:00:00 2001 From: Stephen Horvath Date: Fri, 5 May 2023 09:04:24 +1000 Subject: [PATCH 15/19] Fix site.webmanifest + add svg as favicon (#6304) --- web/images/{safari-pinned-tab.svg => favicon.svg} | 0 web/index.html | 3 ++- .../icons}/android-chrome-192x192.png | Bin .../icons}/android-chrome-512x512.png | Bin web/site.webmanifest | 8 ++++---- 5 files changed, 6 insertions(+), 5 deletions(-) rename web/images/{safari-pinned-tab.svg => favicon.svg} (100%) rename web/{images => public/icons}/android-chrome-192x192.png (100%) rename web/{images => public/icons}/android-chrome-512x512.png (100%) diff --git a/web/images/safari-pinned-tab.svg b/web/images/favicon.svg similarity index 100% rename from web/images/safari-pinned-tab.svg rename to web/images/favicon.svg diff --git a/web/index.html b/web/index.html index db87d3536..bd3e829e1 100644 --- a/web/index.html +++ b/web/index.html @@ -8,8 +8,9 @@ + - + diff --git a/web/images/android-chrome-192x192.png b/web/public/icons/android-chrome-192x192.png similarity index 100% rename from web/images/android-chrome-192x192.png rename to web/public/icons/android-chrome-192x192.png diff --git a/web/images/android-chrome-512x512.png b/web/public/icons/android-chrome-512x512.png similarity index 100% rename from web/images/android-chrome-512x512.png rename to web/public/icons/android-chrome-512x512.png diff --git a/web/site.webmanifest b/web/site.webmanifest index 91aedef05..1f44b8456 100644 --- a/web/site.webmanifest +++ b/web/site.webmanifest @@ -1,14 +1,14 @@ { - "name": "", - "short_name": "", + "name": "Frigate", + "short_name": "Frigate", "icons": [ { - "src": "/images/android-chrome-192x192.png", + "src": "/icons/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { - "src": "/images/android-chrome-512x512.png", + "src": "/icons/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } From ede1dedbbd24da20efd52830e865b182a4e1b167 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Fri, 5 May 2023 02:06:07 +0300 Subject: [PATCH 16/19] Add Deepstack/CodeProject-AI.Server detector plugin (#6143) * Add Deepstack detector plugin with configurable API URL, timeout, and API key * Update DeepStack plugin to recognize 'truck' as 'car' for label indexing * Add debug logging to DeepStack plugin for better monitoring and troubleshooting * Refactor DeepStack label loading from file to use merged labelmap * Black format * add documentation draft * fix link to codeproject website * Apply suggestions from code review Co-authored-by: Nicolas Mowen --------- Co-authored-by: Nicolas Mowen --- docs/docs/configuration/detectors.md | 22 ++++++++ frigate/detectors/plugins/deepstack.py | 78 ++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 frigate/detectors/plugins/deepstack.py diff --git a/docs/docs/configuration/detectors.md b/docs/docs/configuration/detectors.md index b7f442c31..7fbd03f79 100644 --- a/docs/docs/configuration/detectors.md +++ b/docs/docs/configuration/detectors.md @@ -256,3 +256,25 @@ model: width: 416 height: 416 ``` + +## Deepstack / CodeProject.AI Server Detector + +The Deepstack / CodeProject.AI Server detector for Frigate allows you to integrate Deepstack and CodeProject.AI object detection capabilities into Frigate. CodeProject.AI and DeepStack are open-source AI platforms that can be run on various devices such as the Raspberry Pi, Nvidia Jetson, and other compatible hardware. It is important to note that the integration is performed over the network, so the inference times may not be as fast as native Frigate detectors, but it still provides an efficient and reliable solution for object detection and tracking. + +### Setup + +To get started with CodeProject.AI, visit their [official website](https://www.codeproject.com/Articles/5322557/CodeProject-AI-Server-AI-the-easy-way) to follow the instructions to download and install the AI server on your preferred device. Detailed setup instructions for CodeProject.AI are outside the scope of the Frigate documentation. + +To integrate CodeProject.AI into Frigate, you'll need to make the following changes to your Frigate configuration file: + +```yaml +detectors: + deepstack: + api_url: http://:/v1/vision/detection + type: deepstack + api_timeout: 0.1 # seconds +``` + +Replace `` and `` with the IP address and port of your CodeProject.AI server. + +To verify that the integration is working correctly, start Frigate and observe the logs for any error messages related to CodeProject.AI. Additionally, you can check the Frigate web interface to see if the objects detected by CodeProject.AI are being displayed and tracked properly. \ No newline at end of file diff --git a/frigate/detectors/plugins/deepstack.py b/frigate/detectors/plugins/deepstack.py new file mode 100644 index 000000000..716065903 --- /dev/null +++ b/frigate/detectors/plugins/deepstack.py @@ -0,0 +1,78 @@ +import logging +import numpy as np +import requests +import io + +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detector_config import BaseDetectorConfig +from typing import Literal +from pydantic import Extra, Field +from PIL import Image + + +logger = logging.getLogger(__name__) + +DETECTOR_KEY = "deepstack" + + +class DeepstackDetectorConfig(BaseDetectorConfig): + type: Literal[DETECTOR_KEY] + api_url: str = Field( + default="http://localhost:80/v1/vision/detection", title="DeepStack API URL" + ) + api_timeout: float = Field(default=0.1, title="DeepStack API timeout (in seconds)") + api_key: str = Field(default="", title="DeepStack API key (if required)") + + +class DeepStack(DetectionApi): + type_key = DETECTOR_KEY + + def __init__(self, detector_config: DeepstackDetectorConfig): + self.api_url = detector_config.api_url + self.api_timeout = detector_config.api_timeout + self.api_key = detector_config.api_key + self.labels = detector_config.model.merged_labelmap + + self.h = detector_config.model.height + self.w = detector_config.model.width + + def get_label_index(self, label_value): + if label_value.lower() == "truck": + label_value = "car" + for index, value in self.labels.items(): + if value == label_value.lower(): + return index + return -1 + + def detect_raw(self, tensor_input): + image_data = np.squeeze(tensor_input).astype(np.uint8) + image = Image.fromarray(image_data) + with io.BytesIO() as output: + image.save(output, format="JPEG") + image_bytes = output.getvalue() + data = {"api_key": self.api_key} + response = requests.post( + self.api_url, files={"image": image_bytes}, timeout=self.api_timeout + ) + response_json = response.json() + detections = np.zeros((20, 6), np.float32) + + for i, detection in enumerate(response_json["predictions"]): + logger.debug(f"Response: {detection}") + if detection["confidence"] < 0.4: + logger.debug(f"Break due to confidence < 0.4") + break + label = self.get_label_index(detection["label"]) + if label < 0: + logger.debug(f"Break due to unknown label") + break + detections[i] = [ + label, + float(detection["confidence"]), + detection["y_min"] / self.h, + detection["x_min"] / self.w, + detection["y_max"] / self.h, + detection["x_max"] / self.w, + ] + + return detections From aded314f3c80aac3023aeb21c172c5e8ad7826d0 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Thu, 4 May 2023 17:34:48 -0600 Subject: [PATCH 17/19] Timeline UI optimizations (#6367) * Fix centering of bottom_center * Make bottom_center overlap vertically * Fix timeline icon behavior * Make disclaimer hidden under aria label * Don't literally say disclaimer --- web/src/components/Button.jsx | 28 ++++++++++++++------------ web/src/components/TimelineSummary.jsx | 21 ++++++++++++++----- web/src/icons/About.jsx | 1 - web/src/routes/Events.jsx | 2 +- 4 files changed, 32 insertions(+), 20 deletions(-) diff --git a/web/src/components/Button.jsx b/web/src/components/Button.jsx index cb7333a94..a47f93265 100644 --- a/web/src/components/Button.jsx +++ b/web/src/components/Button.jsx @@ -7,48 +7,49 @@ const ButtonColors = { contained: 'bg-blue-500 focus:bg-blue-400 active:bg-blue-600 ring-blue-300', outlined: 'text-blue-500 border-2 border-blue-500 hover:bg-blue-500 hover:bg-opacity-20 focus:bg-blue-500 focus:bg-opacity-40 active:bg-blue-500 active:bg-opacity-40', - text: - 'text-blue-500 hover:bg-blue-500 hover:bg-opacity-20 focus:bg-blue-500 focus:bg-opacity-40 active:bg-blue-500 active:bg-opacity-40', + text: 'text-blue-500 hover:bg-blue-500 hover:bg-opacity-20 focus:bg-blue-500 focus:bg-opacity-40 active:bg-blue-500 active:bg-opacity-40', + iconOnly: 'text-blue-500 hover:text-blue-200', }, red: { contained: 'bg-red-500 focus:bg-red-400 active:bg-red-600 ring-red-300', outlined: 'text-red-500 border-2 border-red-500 hover:bg-red-500 hover:bg-opacity-20 focus:bg-red-500 focus:bg-opacity-40 active:bg-red-500 active:bg-opacity-40', - text: - 'text-red-500 hover:bg-red-500 hover:bg-opacity-20 focus:bg-red-500 focus:bg-opacity-40 active:bg-red-500 active:bg-opacity-40', + text: 'text-red-500 hover:bg-red-500 hover:bg-opacity-20 focus:bg-red-500 focus:bg-opacity-40 active:bg-red-500 active:bg-opacity-40', + iconOnly: 'text-red-500 hover:text-red-200', }, yellow: { contained: 'bg-yellow-500 focus:bg-yellow-400 active:bg-yellow-600 ring-yellow-300', outlined: 'text-yellow-500 border-2 border-yellow-500 hover:bg-yellow-500 hover:bg-opacity-20 focus:bg-yellow-500 focus:bg-opacity-40 active:bg-yellow-500 active:bg-opacity-40', - text: - 'text-yellow-500 hover:bg-yellow-500 hover:bg-opacity-20 focus:bg-yellow-500 focus:bg-opacity-40 active:bg-yellow-500 active:bg-opacity-40', + text: 'text-yellow-500 hover:bg-yellow-500 hover:bg-opacity-20 focus:bg-yellow-500 focus:bg-opacity-40 active:bg-yellow-500 active:bg-opacity-40', + iconOnly: 'text-yellow-500 hover:text-yellow-200', }, green: { contained: 'bg-green-500 focus:bg-green-400 active:bg-green-600 ring-green-300', outlined: 'text-green-500 border-2 border-green-500 hover:bg-green-500 hover:bg-opacity-20 focus:bg-green-500 focus:bg-opacity-40 active:bg-green-500 active:bg-opacity-40', - text: - 'text-green-500 hover:bg-green-500 hover:bg-opacity-20 focus:bg-green-500 focus:bg-opacity-40 active:bg-green-500 active:bg-opacity-40', + text: 'text-green-500 hover:bg-green-500 hover:bg-opacity-20 focus:bg-green-500 focus:bg-opacity-40 active:bg-green-500 active:bg-opacity-40', + iconOnly: 'text-green-500 hover:text-green-200', }, gray: { contained: 'bg-gray-500 focus:bg-gray-400 active:bg-gray-600 ring-gray-300', outlined: 'text-gray-500 border-2 border-gray-500 hover:bg-gray-500 hover:bg-opacity-20 focus:bg-gray-500 focus:bg-opacity-40 active:bg-gray-500 active:bg-opacity-40', - text: - 'text-gray-500 hover:bg-gray-500 hover:bg-opacity-20 focus:bg-gray-500 focus:bg-opacity-40 active:bg-gray-500 active:bg-opacity-40', + text: 'text-gray-500 hover:bg-gray-500 hover:bg-opacity-20 focus:bg-gray-500 focus:bg-opacity-40 active:bg-gray-500 active:bg-opacity-40', + iconOnly: 'text-gray-500 hover:text-gray-200', }, disabled: { contained: 'bg-gray-400', outlined: 'text-gray-500 border-2 border-gray-500 hover:bg-gray-500 hover:bg-opacity-20 focus:bg-gray-500 focus:bg-opacity-40 active:bg-gray-500 active:bg-opacity-40', - text: - 'text-gray-500 hover:bg-gray-500 hover:bg-opacity-20 focus:bg-gray-500 focus:bg-opacity-40 active:bg-gray-500 active:bg-opacity-40', + text: 'text-gray-500 hover:bg-gray-500 hover:bg-opacity-20 focus:bg-gray-500 focus:bg-opacity-40 active:bg-gray-500 active:bg-opacity-40', + iconOnly: 'text-gray-500 hover:text-gray-200', }, black: { contained: '', outlined: '', text: 'text-black dark:text-white', + iconOnly: '', }, }; @@ -56,6 +57,7 @@ const ButtonTypes = { contained: 'text-white shadow focus:shadow-xl hover:shadow-md', outlined: '', text: 'transition-opacity', + iconOnly: 'transition-opacity', }; export default function Button({ @@ -73,7 +75,7 @@ export default function Button({ let classes = `whitespace-nowrap flex items-center space-x-1 ${className} ${ButtonTypes[type]} ${ ButtonColors[disabled ? 'disabled' : color][type] } font-sans inline-flex font-bold uppercase text-xs px-1.5 md:px-2 py-2 rounded outline-none focus:outline-none ring-opacity-50 transition-shadow transition-colors ${ - disabled ? 'cursor-not-allowed' : 'focus:ring-2 cursor-pointer' + disabled ? 'cursor-not-allowed' : `${type == 'iconOnly' ? '' : 'focus:ring-2'} cursor-pointer` }`; if (disabled) { diff --git a/web/src/components/TimelineSummary.jsx b/web/src/components/TimelineSummary.jsx index 7a16bb1aa..8bf5bd1d7 100644 --- a/web/src/components/TimelineSummary.jsx +++ b/web/src/components/TimelineSummary.jsx @@ -2,6 +2,7 @@ import { h } from 'preact'; import useSWR from 'swr'; import ActivityIndicator from './ActivityIndicator'; import { formatUnixTimestampToDateTime } from '../utils/dateUtil'; +import About from '../icons/About'; import PlayIcon from '../icons/Play'; import ExitIcon from '../icons/Exit'; import { Zone } from '../icons/Zone'; @@ -81,7 +82,7 @@ export default function TimelineSummary({ event, onFrameSelected }) {
{timeIndex >= 0 ? ( -
- Disclaimer: This data comes from the detect feed but is shown on the recordings, it is unlikely that the - streams are perfectly in sync so the bounding box and the footage will not line up perfectly. +
+
+
Bounding boxes may not align
+ +
) : null}
diff --git a/web/src/icons/About.jsx b/web/src/icons/About.jsx index 444ad3227..6271b2d52 100644 --- a/web/src/icons/About.jsx +++ b/web/src/icons/About.jsx @@ -17,4 +17,3 @@ export function About({ className = '' }) { } export default memo(About); - diff --git a/web/src/routes/Events.jsx b/web/src/routes/Events.jsx index 30e3507f6..9c479f8b8 100644 --- a/web/src/routes/Events.jsx +++ b/web/src/routes/Events.jsx @@ -724,7 +724,7 @@ export default function Events({ path, ...props }) { }} > {eventOverlay.class_type == 'entered_zone' ? ( -
+
) : null}
) : null} From f52b1212cd3d66ff12c714938193cecbad5d96f8 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Fri, 5 May 2023 05:04:06 +0300 Subject: [PATCH 18/19] add go2rtc version (#6390) Update System.jsx --- docker/rootfs/usr/local/nginx/conf/nginx.conf | 9 +++++++++ web/src/routes/System.jsx | 20 +++++++++++-------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/docker/rootfs/usr/local/nginx/conf/nginx.conf b/docker/rootfs/usr/local/nginx/conf/nginx.conf index 72725efe4..a6b50e317 100644 --- a/docker/rootfs/usr/local/nginx/conf/nginx.conf +++ b/docker/rootfs/usr/local/nginx/conf/nginx.conf @@ -204,6 +204,15 @@ http { proxy_set_header Host $host; } + location ~* /api/go2rtc([/]?.*)$ { + proxy_pass http://go2rtc; + rewrite ^/api/go2rtc(.*)$ /api$1 break; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + } + location ~* /api/.*\.(jpg|jpeg|png)$ { add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS'; diff --git a/web/src/routes/System.jsx b/web/src/routes/System.jsx index 4eeaa71a7..7ec238596 100644 --- a/web/src/routes/System.jsx +++ b/web/src/routes/System.jsx @@ -39,6 +39,8 @@ export default function System() { const cameraNames = Object.keys(cameras || emptyObject); const processesNames = Object.keys(processes || emptyObject); + const { data: go2rtc } = useSWR('go2rtc'); + const onHandleFfprobe = async (camera, e) => { if (e) { e.stopPropagation(); @@ -93,14 +95,16 @@ export default function System() { System {service.version} {config && ( - - go2rtc dashboard - + go2rtc {go2rtc && ( `${go2rtc.version} ` ) } + + dashboard + + )}
From e3b9998879c437dbad03252136f277644828f9f7 Mon Sep 17 00:00:00 2001 From: Blake Blackshear Date: Fri, 5 May 2023 07:45:13 -0500 Subject: [PATCH 19/19] Upgrade deps (#6395) * update js build tools * update frontend deps * python updates * update requests again --- requirements-wheels.txt | 6 +- web/package-lock.json | 888 +++++++++++++++++----------------------- web/package.json | 16 +- web/src/api/ws.jsx | 2 +- 4 files changed, 377 insertions(+), 535 deletions(-) diff --git a/requirements-wheels.txt b/requirements-wheels.txt index 95d70077a..d785df5d6 100644 --- a/requirements-wheels.txt +++ b/requirements-wheels.txt @@ -1,5 +1,5 @@ click == 8.1.* -Flask == 2.2.* +Flask == 2.3.* imutils == 0.5.* matplotlib == 3.7.* mypy == 0.942 @@ -7,7 +7,7 @@ numpy == 1.23.* onvif_zeep == 0.2.12 opencv-python-headless == 4.5.5.* paho-mqtt == 1.6.* -peewee == 3.15.* +peewee == 3.16.* peewee_migrate == 1.7.* psutil == 5.9.* pydantic == 1.10.* @@ -16,7 +16,7 @@ PyYAML == 6.0 pytz == 2023.3 tzlocal == 4.3 types-PyYAML == 6.0.* -requests == 2.28.* +requests == 2.30.* types-requests == 2.28.* scipy == 1.10.* setproctitle == 1.3.* diff --git a/web/package-lock.json b/web/package-lock.json index d3a42cb90..26720e3ea 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -9,11 +9,11 @@ "version": "0.0.0", "dependencies": { "@cycjimmy/jsmpeg-player": "^6.0.5", - "axios": "^1.3.6", + "axios": "^1.4.0", "copy-to-clipboard": "3.3.3", - "date-fns": "^2.29.3", + "date-fns": "^2.30.0", "idb-keyval": "^6.2.0", - "immer": "^9.0.21", + "immer": "^10.0.1", "monaco-yaml": "^4.0.4", "preact": "^10.13.2", "preact-async-route": "^2.2.1", @@ -34,22 +34,22 @@ "@testing-library/user-event": "^14.4.3", "@typescript-eslint/eslint-plugin": "^5.59.1", "@typescript-eslint/parser": "^5.59.1", - "@vitest/coverage-c8": "^0.30.1", - "@vitest/ui": "^0.30.1", + "@vitest/coverage-c8": "^0.31.0", + "@vitest/ui": "^0.31.0", "autoprefixer": "^10.4.14", "eslint": "^8.39.0", "eslint-config-preact": "^1.3.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-vitest-globals": "^1.3.1", "fake-indexeddb": "^4.0.1", - "jsdom": "^21.1.1", + "jsdom": "^22.0.0", "msw": "^1.2.1", "postcss": "^8.4.23", "prettier": "^2.8.8", "tailwindcss": "^3.3.2", "typescript": "^5.0.4", - "vite": "^4.3.2", - "vitest": "^0.30.1" + "vite": "^4.3.5", + "vitest": "^0.31.0" } }, "node_modules/@adobe/css-tools": { @@ -461,11 +461,11 @@ } }, "node_modules/@babel/runtime": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.1.tgz", - "integrity": "sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg==", + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz", + "integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==", "dependencies": { - "regenerator-runtime": "^0.13.10" + "regenerator-runtime": "^0.13.11" }, "engines": { "node": ">=6.9.0" @@ -1787,15 +1787,15 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.1.tgz", - "integrity": "sha512-AVi0uazY5quFB9hlp2Xv+ogpfpk77xzsgsIEWyVS7uK/c7MZ5tw7ZPbapa0SbfkqE0fsAMkz5UwtgMLVk2BQAg==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.2.tgz", + "integrity": "sha512-yVrXupeHjRxLDcPKL10sGQ/QlVrA8J5IYOEWVqk0lJaSZP7X5DfnP7Ns3cc74/blmbipQ1htFNVGsHX6wsYm0A==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.1", - "@typescript-eslint/type-utils": "5.59.1", - "@typescript-eslint/utils": "5.59.1", + "@typescript-eslint/scope-manager": "5.59.2", + "@typescript-eslint/type-utils": "5.59.2", + "@typescript-eslint/utils": "5.59.2", "debug": "^4.3.4", "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", @@ -1821,13 +1821,13 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz", - "integrity": "sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz", + "integrity": "sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/visitor-keys": "5.59.1" + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1838,9 +1838,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.1.tgz", - "integrity": "sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.2.tgz", + "integrity": "sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1851,13 +1851,13 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz", - "integrity": "sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz", + "integrity": "sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/visitor-keys": "5.59.1", + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1878,17 +1878,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.1.tgz", - "integrity": "sha512-MkTe7FE+K1/GxZkP5gRj3rCztg45bEhsd8HYjczBuYm+qFHP5vtZmjx3B0yUCDotceQ4sHgTyz60Ycl225njmA==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.2.tgz", + "integrity": "sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.1", - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/typescript-estree": "5.59.1", + "@typescript-eslint/scope-manager": "5.59.2", + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/typescript-estree": "5.59.2", "eslint-scope": "^5.1.1", "semver": "^7.3.7" }, @@ -1904,12 +1904,12 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz", - "integrity": "sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz", + "integrity": "sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/types": "5.59.2", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -1977,14 +1977,14 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.1.tgz", - "integrity": "sha512-nzjFAN8WEu6yPRDizIFyzAfgK7nybPodMNFGNH0M9tei2gYnYszRDqVA0xlnRjkl7Hkx2vYrEdb6fP2a21cG1g==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.2.tgz", + "integrity": "sha512-uq0sKyw6ao1iFOZZGk9F8Nro/8+gfB5ezl1cA06SrqbgJAt0SRoFhb9pXaHvkrxUpZaoLxt8KlovHNk8Gp6/HQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.59.1", - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/typescript-estree": "5.59.1", + "@typescript-eslint/scope-manager": "5.59.2", + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/typescript-estree": "5.59.2", "debug": "^4.3.4" }, "engines": { @@ -2004,13 +2004,13 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz", - "integrity": "sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz", + "integrity": "sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/visitor-keys": "5.59.1" + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2021,9 +2021,9 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.1.tgz", - "integrity": "sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.2.tgz", + "integrity": "sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2034,13 +2034,13 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz", - "integrity": "sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz", + "integrity": "sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/visitor-keys": "5.59.1", + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2061,12 +2061,12 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz", - "integrity": "sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz", + "integrity": "sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/types": "5.59.2", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -2110,13 +2110,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.1.tgz", - "integrity": "sha512-ZMWQ+Oh82jWqWzvM3xU+9y5U7MEMVv6GLioM3R5NJk6uvP47kZ7YvlgSHJ7ERD6bOY7Q4uxWm25c76HKEwIjZw==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.2.tgz", + "integrity": "sha512-b1LS2phBOsEy/T381bxkkywfQXkV1dWda/z0PhnIy3bC5+rQWQDS7fk9CSpcXBccPY27Z6vBEuaPBCKCgYezyQ==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.59.1", - "@typescript-eslint/utils": "5.59.1", + "@typescript-eslint/typescript-estree": "5.59.2", + "@typescript-eslint/utils": "5.59.2", "debug": "^4.3.4", "tsutils": "^3.21.0" }, @@ -2137,13 +2137,13 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz", - "integrity": "sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz", + "integrity": "sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/visitor-keys": "5.59.1" + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2154,9 +2154,9 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.1.tgz", - "integrity": "sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.2.tgz", + "integrity": "sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2167,13 +2167,13 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz", - "integrity": "sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz", + "integrity": "sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/visitor-keys": "5.59.1", + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2194,17 +2194,17 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.1.tgz", - "integrity": "sha512-MkTe7FE+K1/GxZkP5gRj3rCztg45bEhsd8HYjczBuYm+qFHP5vtZmjx3B0yUCDotceQ4sHgTyz60Ycl225njmA==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.2.tgz", + "integrity": "sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.1", - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/typescript-estree": "5.59.1", + "@typescript-eslint/scope-manager": "5.59.2", + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/typescript-estree": "5.59.2", "eslint-scope": "^5.1.1", "semver": "^7.3.7" }, @@ -2220,12 +2220,12 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz", - "integrity": "sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz", + "integrity": "sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/types": "5.59.2", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -2455,43 +2455,51 @@ } }, "node_modules/@vitest/coverage-c8": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/@vitest/coverage-c8/-/coverage-c8-0.30.1.tgz", - "integrity": "sha512-/Wa3dtSuckpdngAmiCwowaEXXgJkqPrtfvrs9HTB9QoEfNbZWPu4E4cjEn4lJZb4qcGf4fxFtUA2f9DnDNAzBA==", + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vitest/coverage-c8/-/coverage-c8-0.31.0.tgz", + "integrity": "sha512-h72qN1D962AO7UefQVulm9JFP5ACS7OfhCdBHioXU8f7ohH/+NTZCgAqmgcfRNHHO/8wLFxx+93YVxhodkEJVA==", "dev": true, "dependencies": { + "@ampproject/remapping": "^2.2.0", "c8": "^7.13.0", + "magic-string": "^0.30.0", "picocolors": "^1.0.0", "std-env": "^3.3.2" }, "funding": { - "url": "https://github.com/sponsors/antfu" + "url": "https://opencollective.com/vitest" }, "peerDependencies": { "vitest": ">=0.30.0 <1" } }, "node_modules/@vitest/expect": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.30.1.tgz", - "integrity": "sha512-c3kbEtN8XXJSeN81iDGq29bUzSjQhjES2WR3aColsS4lPGbivwLtas4DNUe0jD9gg/FYGIteqOenfU95EFituw==", + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.31.0.tgz", + "integrity": "sha512-Jlm8ZTyp6vMY9iz9Ny9a0BHnCG4fqBa8neCF6Pk/c/6vkUk49Ls6UBlgGAU82QnzzoaUs9E/mUhq/eq9uMOv/g==", "dev": true, "dependencies": { - "@vitest/spy": "0.30.1", - "@vitest/utils": "0.30.1", + "@vitest/spy": "0.31.0", + "@vitest/utils": "0.31.0", "chai": "^4.3.7" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.30.1.tgz", - "integrity": "sha512-W62kT/8i0TF1UBCNMRtRMOBWJKRnNyv9RrjIgdUryEe0wNpGZvvwPDLuzYdxvgSckzjp54DSpv1xUbv4BQ0qVA==", + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.31.0.tgz", + "integrity": "sha512-H1OE+Ly7JFeBwnpHTrKyCNm/oZgr+16N4qIlzzqSG/YRQDATBYmJb/KUn3GrZaiQQyL7GwpNHVZxSQd6juLCgw==", "dev": true, "dependencies": { - "@vitest/utils": "0.30.1", + "@vitest/utils": "0.31.0", "concordance": "^5.0.4", "p-limit": "^4.0.0", "pathe": "^1.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner/node_modules/p-limit": { @@ -2522,49 +2530,64 @@ } }, "node_modules/@vitest/snapshot": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.30.1.tgz", - "integrity": "sha512-fJZqKrE99zo27uoZA/azgWyWbFvM1rw2APS05yB0JaLwUIg9aUtvvnBf4q7JWhEcAHmSwbrxKFgyBUga6tq9Tw==", + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.31.0.tgz", + "integrity": "sha512-5dTXhbHnyUMTMOujZPB0wjFjQ6q5x9c8TvAsSPUNKjp1tVU7i9pbqcKPqntyu2oXtmVxKbuHCqrOd+Ft60r4tg==", "dev": true, "dependencies": { "magic-string": "^0.30.0", "pathe": "^1.1.0", "pretty-format": "^27.5.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/spy": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.30.1.tgz", - "integrity": "sha512-YfJeIf37GvTZe04ZKxzJfnNNuNSmTEGnla2OdL60C8od16f3zOfv9q9K0nNii0NfjDJRt/CVN/POuY5/zTS+BA==", + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.31.0.tgz", + "integrity": "sha512-IzCEQ85RN26GqjQNkYahgVLLkULOxOm5H/t364LG0JYb3Apg0PsYCHLBYGA006+SVRMWhQvHlBBCyuByAMFmkg==", "dev": true, "dependencies": { "tinyspy": "^2.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/ui": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-0.30.1.tgz", - "integrity": "sha512-Izz4ElDmdvX02KImSC2nCJI6CsGo9aETbKqxli55M0rbbPPAMtF0zDcJIqgEP5V6Y+4Ysf6wvsjLbLCTnaBvKw==", + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-0.31.0.tgz", + "integrity": "sha512-Dy86l6r3/dbJposgm7w+oqb/15UWJ0lDBbEQaS1ived3+0CTaMbT8OMkUf9vNBkSL47kvBHEBnZLa5fw5i9gUQ==", "dev": true, "dependencies": { - "@vitest/utils": "0.30.1", + "@vitest/utils": "0.31.0", "fast-glob": "^3.2.12", "fflate": "^0.7.4", "flatted": "^3.2.7", "pathe": "^1.1.0", "picocolors": "^1.0.0", "sirv": "^2.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": ">=0.30.1 <1" } }, "node_modules/@vitest/utils": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.30.1.tgz", - "integrity": "sha512-/c8Xv2zUVc+rnNt84QF0Y0zkfxnaGhp87K2dYJMLtLOIckPzuxLVzAtFCicGFdB4NeBHNzTRr1tNn7rCtQcWFA==", + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.31.0.tgz", + "integrity": "sha512-kahaRyLX7GS1urekRXN2752X4gIgOGVX4Wo8eDUGUkTWlGpXzf5ZS6N9RUUS+Re3XEE8nVGqNyxkSxF5HXlGhQ==", "dev": true, "dependencies": { "concordance": "^5.0.4", "loupe": "^2.3.6", "pretty-format": "^27.5.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, "node_modules/@xmldom/xmldom": { @@ -2600,16 +2623,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-globals": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", - "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", - "dev": true, - "dependencies": { - "acorn": "^8.1.0", - "acorn-walk": "^8.0.2" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -2898,9 +2911,9 @@ } }, "node_modules/axios": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.6.tgz", - "integrity": "sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", "dependencies": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -3444,9 +3457,9 @@ } }, "node_modules/concordance/node_modules/semver": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.4.0.tgz", - "integrity": "sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -3584,9 +3597,12 @@ } }, "node_modules/date-fns": { - "version": "2.29.3", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", - "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==", + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, "engines": { "node": ">=0.11" }, @@ -3631,9 +3647,9 @@ "dev": true }, "node_modules/deep-eql": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.2.tgz", - "integrity": "sha512-gT18+YW4CcW/DBNTwAmqTtkJh7f9qqScu2qFVlx7kCoeY9tlBu9cUcr7+I+Z/noG8INehS3xQgLpTtd/QUTn4w==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", "dev": true, "dependencies": { "type-detect": "^4.0.0" @@ -3952,79 +3968,6 @@ "node": ">=0.8.0" } }, - "node_modules/escodegen": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", - "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/escodegen/node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", - "dev": true, - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/escodegen/node_modules/optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/escodegen/node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/escodegen/node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", - "dev": true, - "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/eslint": { "version": "8.39.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz", @@ -4431,19 +4374,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/esquery": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", @@ -5221,9 +5151,9 @@ } }, "node_modules/immer": { - "version": "9.0.21", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", - "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.1.tgz", + "integrity": "sha512-zg++jJLsKKTwXGeSYIw0HgChSYQGtu0UDTnbKx5aGLYgte4CwTmH9eJDYyQ6FheyUtBe+lQW9FrGxya1G+Dtmg==", "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -6321,25 +6251,22 @@ } }, "node_modules/jsdom": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-21.1.1.tgz", - "integrity": "sha512-Jjgdmw48RKcdAIQyUD1UdBh2ecH7VqwaXPN3ehoZN6MqgVbMn+lRm1aAT1AsdJRAJpwfa4IpwgzySn61h2qu3w==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.0.0.tgz", + "integrity": "sha512-p5ZTEb5h+O+iU02t0GfEjAnkdYPrQSkfuTSMkMYyIoMvUNEHsbG0bHHbfXIcfTqD2UfvjQX7mmgiFsyRwGscVw==", "dev": true, "dependencies": { "abab": "^2.0.6", - "acorn": "^8.8.2", - "acorn-globals": "^7.0.0", "cssstyle": "^3.0.0", "data-urls": "^4.0.0", "decimal.js": "^10.4.3", "domexception": "^4.0.0", - "escodegen": "^2.0.0", "form-data": "^4.0.0", "html-encoding-sniffer": "^3.0.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.1", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.2", + "nwsapi": "^2.2.4", "parse5": "^7.1.2", "rrweb-cssom": "^0.6.0", "saxes": "^6.0.0", @@ -6354,7 +6281,7 @@ "xml-name-validator": "^4.0.0" }, "engines": { - "node": ">=14" + "node": ">=16" }, "peerDependencies": { "canvas": "^2.5.0" @@ -7154,9 +7081,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz", - "integrity": "sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.4.tgz", + "integrity": "sha512-NHj4rzRo0tQdijE9ZqAx6kYDcoRwYwSYzCA8MY3JzfxlrvEU0jhnhJT9BhqhJs7I/dKcrDm6TyulaRqZPIhN5g==", "dev": true }, "node_modules/object-assign": { @@ -7617,13 +7544,13 @@ } }, "node_modules/pkg-types": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.2.tgz", - "integrity": "sha512-hM58GKXOcj8WTqUXnsQyJYXdeAPbythQgEF3nTcEo+nkD49chjQ9IKm/QJy9xf6JakXptz86h7ecP2024rrLaQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", "dev": true, "dependencies": { "jsonc-parser": "^3.2.0", - "mlly": "^1.1.1", + "mlly": "^1.2.0", "pathe": "^1.1.0" } }, @@ -8038,9 +7965,9 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.13.10", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz", - "integrity": "sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw==" + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, "node_modules/regexp.prototype.flags": { "version": "1.4.3", @@ -8345,15 +8272,6 @@ "node": ">=8" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", @@ -8785,9 +8703,9 @@ "dev": true }, "node_modules/tinypool": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.4.0.tgz", - "integrity": "sha512-2ksntHOKf893wSAH4z/+JbPpi92esw8Gn9N2deXX+B0EO92hexAVI9GIZZPx7P5aYo5KULfeOSt3kMOmSOy6uA==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.5.0.tgz", + "integrity": "sha512-paHQtnrlS1QZYKF/GnLoOM/DN9fqaGOFbCbxzAhwniySnzl9Ebk8w73/dd34DAhe/obUbPAOldTyYXQZxnPBPQ==", "dev": true, "engines": { "node": ">=14.0.0" @@ -9016,9 +8934,9 @@ } }, "node_modules/ufo": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.1.1.tgz", - "integrity": "sha512-MvlCc4GHrmZdAllBc0iUDowff36Q9Ndw/UzqmEKyrfSzokTd9ZCy1i+IIk5hrYKkjoYVQyNbrw7/F8XJ2rEwTg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.1.2.tgz", + "integrity": "sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==", "dev": true }, "node_modules/unbox-primitive": { @@ -9190,13 +9108,13 @@ } }, "node_modules/vite": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.2.tgz", - "integrity": "sha512-9R53Mf+TBoXCYejcL+qFbZde+eZveQLDYd9XgULILLC1a5ZwPaqgmdVpL8/uvw2BM/1TzetWjglwm+3RO+xTyw==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.5.tgz", + "integrity": "sha512-0gEnL9wiRFxgz40o/i/eTBwm+NEbpUeTWhzKrZDSdKm6nplj+z4lKz8ANDgildxHm47Vg8EUia0aicKbawUVVA==", "dev": true, "dependencies": { "esbuild": "^0.17.5", - "postcss": "^8.4.21", + "postcss": "^8.4.23", "rollup": "^3.21.0" }, "bin": { @@ -9238,9 +9156,9 @@ } }, "node_modules/vite-node": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.30.1.tgz", - "integrity": "sha512-vTikpU/J7e6LU/8iM3dzBo8ZhEiKZEKRznEMm+mJh95XhWaPrJQraT/QsT2NWmuEf+zgAoMe64PKT7hfZ1Njmg==", + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.31.0.tgz", + "integrity": "sha512-8x1x1LNuPvE2vIvkSB7c1mApX5oqlgsxzHQesYF7l5n1gKrEmrClIiZuOFbFDQcjLsmcWSwwmrWrcGWm9Fxc/g==", "dev": true, "dependencies": { "cac": "^6.7.14", @@ -9257,7 +9175,7 @@ "node": ">=v14.18.0" }, "funding": { - "url": "https://github.com/sponsors/antfu" + "url": "https://opencollective.com/vitest" } }, "node_modules/vite-plugin-monaco-editor": { @@ -9269,19 +9187,19 @@ } }, "node_modules/vitest": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.30.1.tgz", - "integrity": "sha512-y35WTrSTlTxfMLttgQk4rHcaDkbHQwDP++SNwPb+7H8yb13Q3cu2EixrtHzF27iZ8v0XCciSsLg00RkPAzB/aA==", + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.31.0.tgz", + "integrity": "sha512-JwWJS9p3GU9GxkG7eBSmr4Q4x4bvVBSswaCFf1PBNHiPx00obfhHRJfgHcnI0ffn+NMlIh9QGvG75FlaIBdKGA==", "dev": true, "dependencies": { "@types/chai": "^4.3.4", "@types/chai-subset": "^1.3.3", "@types/node": "*", - "@vitest/expect": "0.30.1", - "@vitest/runner": "0.30.1", - "@vitest/snapshot": "0.30.1", - "@vitest/spy": "0.30.1", - "@vitest/utils": "0.30.1", + "@vitest/expect": "0.31.0", + "@vitest/runner": "0.31.0", + "@vitest/snapshot": "0.31.0", + "@vitest/spy": "0.31.0", + "@vitest/utils": "0.31.0", "acorn": "^8.8.2", "acorn-walk": "^8.2.0", "cac": "^6.7.14", @@ -9292,13 +9210,12 @@ "magic-string": "^0.30.0", "pathe": "^1.1.0", "picocolors": "^1.0.0", - "source-map": "^0.6.1", "std-env": "^3.3.2", "strip-literal": "^1.0.1", "tinybench": "^2.4.0", - "tinypool": "^0.4.0", + "tinypool": "^0.5.0", "vite": "^3.0.0 || ^4.0.0", - "vite-node": "0.30.1", + "vite-node": "0.31.0", "why-is-node-running": "^2.2.2" }, "bin": { @@ -9308,7 +9225,7 @@ "node": ">=v14.18.0" }, "funding": { - "url": "https://github.com/sponsors/antfu" + "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", @@ -9991,11 +9908,11 @@ } }, "@babel/runtime": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.1.tgz", - "integrity": "sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg==", + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz", + "integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==", "requires": { - "regenerator-runtime": "^0.13.10" + "regenerator-runtime": "^0.13.11" } }, "@babel/template": { @@ -10929,15 +10846,15 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.1.tgz", - "integrity": "sha512-AVi0uazY5quFB9hlp2Xv+ogpfpk77xzsgsIEWyVS7uK/c7MZ5tw7ZPbapa0SbfkqE0fsAMkz5UwtgMLVk2BQAg==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.2.tgz", + "integrity": "sha512-yVrXupeHjRxLDcPKL10sGQ/QlVrA8J5IYOEWVqk0lJaSZP7X5DfnP7Ns3cc74/blmbipQ1htFNVGsHX6wsYm0A==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.1", - "@typescript-eslint/type-utils": "5.59.1", - "@typescript-eslint/utils": "5.59.1", + "@typescript-eslint/scope-manager": "5.59.2", + "@typescript-eslint/type-utils": "5.59.2", + "@typescript-eslint/utils": "5.59.2", "debug": "^4.3.4", "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", @@ -10947,29 +10864,29 @@ }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz", - "integrity": "sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz", + "integrity": "sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/visitor-keys": "5.59.1" + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2" } }, "@typescript-eslint/types": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.1.tgz", - "integrity": "sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.2.tgz", + "integrity": "sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz", - "integrity": "sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz", + "integrity": "sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==", "dev": true, "requires": { - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/visitor-keys": "5.59.1", + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -10978,28 +10895,28 @@ } }, "@typescript-eslint/utils": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.1.tgz", - "integrity": "sha512-MkTe7FE+K1/GxZkP5gRj3rCztg45bEhsd8HYjczBuYm+qFHP5vtZmjx3B0yUCDotceQ4sHgTyz60Ycl225njmA==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.2.tgz", + "integrity": "sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.1", - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/typescript-estree": "5.59.1", + "@typescript-eslint/scope-manager": "5.59.2", + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/typescript-estree": "5.59.2", "eslint-scope": "^5.1.1", "semver": "^7.3.7" } }, "@typescript-eslint/visitor-keys": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz", - "integrity": "sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz", + "integrity": "sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig==", "dev": true, "requires": { - "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/types": "5.59.2", "eslint-visitor-keys": "^3.3.0" } }, @@ -11040,41 +10957,41 @@ } }, "@typescript-eslint/parser": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.1.tgz", - "integrity": "sha512-nzjFAN8WEu6yPRDizIFyzAfgK7nybPodMNFGNH0M9tei2gYnYszRDqVA0xlnRjkl7Hkx2vYrEdb6fP2a21cG1g==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.2.tgz", + "integrity": "sha512-uq0sKyw6ao1iFOZZGk9F8Nro/8+gfB5ezl1cA06SrqbgJAt0SRoFhb9pXaHvkrxUpZaoLxt8KlovHNk8Gp6/HQ==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.59.1", - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/typescript-estree": "5.59.1", + "@typescript-eslint/scope-manager": "5.59.2", + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/typescript-estree": "5.59.2", "debug": "^4.3.4" }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz", - "integrity": "sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz", + "integrity": "sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/visitor-keys": "5.59.1" + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2" } }, "@typescript-eslint/types": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.1.tgz", - "integrity": "sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.2.tgz", + "integrity": "sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz", - "integrity": "sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz", + "integrity": "sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==", "dev": true, "requires": { - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/visitor-keys": "5.59.1", + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -11083,12 +11000,12 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz", - "integrity": "sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz", + "integrity": "sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig==", "dev": true, "requires": { - "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/types": "5.59.2", "eslint-visitor-keys": "^3.3.0" } }, @@ -11114,41 +11031,41 @@ } }, "@typescript-eslint/type-utils": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.1.tgz", - "integrity": "sha512-ZMWQ+Oh82jWqWzvM3xU+9y5U7MEMVv6GLioM3R5NJk6uvP47kZ7YvlgSHJ7ERD6bOY7Q4uxWm25c76HKEwIjZw==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.2.tgz", + "integrity": "sha512-b1LS2phBOsEy/T381bxkkywfQXkV1dWda/z0PhnIy3bC5+rQWQDS7fk9CSpcXBccPY27Z6vBEuaPBCKCgYezyQ==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "5.59.1", - "@typescript-eslint/utils": "5.59.1", + "@typescript-eslint/typescript-estree": "5.59.2", + "@typescript-eslint/utils": "5.59.2", "debug": "^4.3.4", "tsutils": "^3.21.0" }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz", - "integrity": "sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz", + "integrity": "sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/visitor-keys": "5.59.1" + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2" } }, "@typescript-eslint/types": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.1.tgz", - "integrity": "sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.2.tgz", + "integrity": "sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz", - "integrity": "sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz", + "integrity": "sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==", "dev": true, "requires": { - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/visitor-keys": "5.59.1", + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -11157,28 +11074,28 @@ } }, "@typescript-eslint/utils": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.1.tgz", - "integrity": "sha512-MkTe7FE+K1/GxZkP5gRj3rCztg45bEhsd8HYjczBuYm+qFHP5vtZmjx3B0yUCDotceQ4sHgTyz60Ycl225njmA==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.2.tgz", + "integrity": "sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.1", - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/typescript-estree": "5.59.1", + "@typescript-eslint/scope-manager": "5.59.2", + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/typescript-estree": "5.59.2", "eslint-scope": "^5.1.1", "semver": "^7.3.7" } }, "@typescript-eslint/visitor-keys": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz", - "integrity": "sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz", + "integrity": "sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig==", "dev": true, "requires": { - "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/types": "5.59.2", "eslint-visitor-keys": "^3.3.0" } }, @@ -11330,34 +11247,36 @@ } }, "@vitest/coverage-c8": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/@vitest/coverage-c8/-/coverage-c8-0.30.1.tgz", - "integrity": "sha512-/Wa3dtSuckpdngAmiCwowaEXXgJkqPrtfvrs9HTB9QoEfNbZWPu4E4cjEn4lJZb4qcGf4fxFtUA2f9DnDNAzBA==", + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vitest/coverage-c8/-/coverage-c8-0.31.0.tgz", + "integrity": "sha512-h72qN1D962AO7UefQVulm9JFP5ACS7OfhCdBHioXU8f7ohH/+NTZCgAqmgcfRNHHO/8wLFxx+93YVxhodkEJVA==", "dev": true, "requires": { + "@ampproject/remapping": "^2.2.0", "c8": "^7.13.0", + "magic-string": "^0.30.0", "picocolors": "^1.0.0", "std-env": "^3.3.2" } }, "@vitest/expect": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.30.1.tgz", - "integrity": "sha512-c3kbEtN8XXJSeN81iDGq29bUzSjQhjES2WR3aColsS4lPGbivwLtas4DNUe0jD9gg/FYGIteqOenfU95EFituw==", + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.31.0.tgz", + "integrity": "sha512-Jlm8ZTyp6vMY9iz9Ny9a0BHnCG4fqBa8neCF6Pk/c/6vkUk49Ls6UBlgGAU82QnzzoaUs9E/mUhq/eq9uMOv/g==", "dev": true, "requires": { - "@vitest/spy": "0.30.1", - "@vitest/utils": "0.30.1", + "@vitest/spy": "0.31.0", + "@vitest/utils": "0.31.0", "chai": "^4.3.7" } }, "@vitest/runner": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.30.1.tgz", - "integrity": "sha512-W62kT/8i0TF1UBCNMRtRMOBWJKRnNyv9RrjIgdUryEe0wNpGZvvwPDLuzYdxvgSckzjp54DSpv1xUbv4BQ0qVA==", + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.31.0.tgz", + "integrity": "sha512-H1OE+Ly7JFeBwnpHTrKyCNm/oZgr+16N4qIlzzqSG/YRQDATBYmJb/KUn3GrZaiQQyL7GwpNHVZxSQd6juLCgw==", "dev": true, "requires": { - "@vitest/utils": "0.30.1", + "@vitest/utils": "0.31.0", "concordance": "^5.0.4", "p-limit": "^4.0.0", "pathe": "^1.1.0" @@ -11381,9 +11300,9 @@ } }, "@vitest/snapshot": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.30.1.tgz", - "integrity": "sha512-fJZqKrE99zo27uoZA/azgWyWbFvM1rw2APS05yB0JaLwUIg9aUtvvnBf4q7JWhEcAHmSwbrxKFgyBUga6tq9Tw==", + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.31.0.tgz", + "integrity": "sha512-5dTXhbHnyUMTMOujZPB0wjFjQ6q5x9c8TvAsSPUNKjp1tVU7i9pbqcKPqntyu2oXtmVxKbuHCqrOd+Ft60r4tg==", "dev": true, "requires": { "magic-string": "^0.30.0", @@ -11392,21 +11311,21 @@ } }, "@vitest/spy": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.30.1.tgz", - "integrity": "sha512-YfJeIf37GvTZe04ZKxzJfnNNuNSmTEGnla2OdL60C8od16f3zOfv9q9K0nNii0NfjDJRt/CVN/POuY5/zTS+BA==", + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.31.0.tgz", + "integrity": "sha512-IzCEQ85RN26GqjQNkYahgVLLkULOxOm5H/t364LG0JYb3Apg0PsYCHLBYGA006+SVRMWhQvHlBBCyuByAMFmkg==", "dev": true, "requires": { "tinyspy": "^2.1.0" } }, "@vitest/ui": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-0.30.1.tgz", - "integrity": "sha512-Izz4ElDmdvX02KImSC2nCJI6CsGo9aETbKqxli55M0rbbPPAMtF0zDcJIqgEP5V6Y+4Ysf6wvsjLbLCTnaBvKw==", + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-0.31.0.tgz", + "integrity": "sha512-Dy86l6r3/dbJposgm7w+oqb/15UWJ0lDBbEQaS1ived3+0CTaMbT8OMkUf9vNBkSL47kvBHEBnZLa5fw5i9gUQ==", "dev": true, "requires": { - "@vitest/utils": "0.30.1", + "@vitest/utils": "0.31.0", "fast-glob": "^3.2.12", "fflate": "^0.7.4", "flatted": "^3.2.7", @@ -11416,9 +11335,9 @@ } }, "@vitest/utils": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.30.1.tgz", - "integrity": "sha512-/c8Xv2zUVc+rnNt84QF0Y0zkfxnaGhp87K2dYJMLtLOIckPzuxLVzAtFCicGFdB4NeBHNzTRr1tNn7rCtQcWFA==", + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.31.0.tgz", + "integrity": "sha512-kahaRyLX7GS1urekRXN2752X4gIgOGVX4Wo8eDUGUkTWlGpXzf5ZS6N9RUUS+Re3XEE8nVGqNyxkSxF5HXlGhQ==", "dev": true, "requires": { "concordance": "^5.0.4", @@ -11450,16 +11369,6 @@ "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", "dev": true }, - "acorn-globals": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", - "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", - "dev": true, - "requires": { - "acorn": "^8.1.0", - "acorn-walk": "^8.0.2" - } - }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -11674,9 +11583,9 @@ "dev": true }, "axios": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.6.tgz", - "integrity": "sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", "requires": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -12076,9 +11985,9 @@ }, "dependencies": { "semver": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.4.0.tgz", - "integrity": "sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -12185,9 +12094,12 @@ } }, "date-fns": { - "version": "2.29.3", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", - "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==" + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "requires": { + "@babel/runtime": "^7.21.0" + } }, "date-time": { "version": "3.1.0", @@ -12214,9 +12126,9 @@ "dev": true }, "deep-eql": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.2.tgz", - "integrity": "sha512-gT18+YW4CcW/DBNTwAmqTtkJh7f9qqScu2qFVlx7kCoeY9tlBu9cUcr7+I+Z/noG8INehS3xQgLpTtd/QUTn4w==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", "dev": true, "requires": { "type-detect": "^4.0.0" @@ -12475,60 +12387,6 @@ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true }, - "escodegen": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", - "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", - "dev": true, - "requires": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - }, - "dependencies": { - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - } - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "dev": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - } - } - }, "eslint": { "version": "8.39.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz", @@ -12810,12 +12668,6 @@ "eslint-visitor-keys": "^3.4.0" } }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, "esquery": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", @@ -13401,9 +13253,9 @@ "dev": true }, "immer": { - "version": "9.0.21", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", - "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==" + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.1.tgz", + "integrity": "sha512-zg++jJLsKKTwXGeSYIw0HgChSYQGtu0UDTnbKx5aGLYgte4CwTmH9eJDYyQ6FheyUtBe+lQW9FrGxya1G+Dtmg==" }, "import-fresh": { "version": "3.3.0", @@ -14206,25 +14058,22 @@ } }, "jsdom": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-21.1.1.tgz", - "integrity": "sha512-Jjgdmw48RKcdAIQyUD1UdBh2ecH7VqwaXPN3ehoZN6MqgVbMn+lRm1aAT1AsdJRAJpwfa4IpwgzySn61h2qu3w==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.0.0.tgz", + "integrity": "sha512-p5ZTEb5h+O+iU02t0GfEjAnkdYPrQSkfuTSMkMYyIoMvUNEHsbG0bHHbfXIcfTqD2UfvjQX7mmgiFsyRwGscVw==", "dev": true, "requires": { "abab": "^2.0.6", - "acorn": "^8.8.2", - "acorn-globals": "^7.0.0", "cssstyle": "^3.0.0", "data-urls": "^4.0.0", "decimal.js": "^10.4.3", "domexception": "^4.0.0", - "escodegen": "^2.0.0", "form-data": "^4.0.0", "html-encoding-sniffer": "^3.0.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.1", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.2", + "nwsapi": "^2.2.4", "parse5": "^7.1.2", "rrweb-cssom": "^0.6.0", "saxes": "^6.0.0", @@ -14821,9 +14670,9 @@ "dev": true }, "nwsapi": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz", - "integrity": "sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.4.tgz", + "integrity": "sha512-NHj4rzRo0tQdijE9ZqAx6kYDcoRwYwSYzCA8MY3JzfxlrvEU0jhnhJT9BhqhJs7I/dKcrDm6TyulaRqZPIhN5g==", "dev": true }, "object-assign": { @@ -15157,13 +15006,13 @@ } }, "pkg-types": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.2.tgz", - "integrity": "sha512-hM58GKXOcj8WTqUXnsQyJYXdeAPbythQgEF3nTcEo+nkD49chjQ9IKm/QJy9xf6JakXptz86h7ecP2024rrLaQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", "dev": true, "requires": { "jsonc-parser": "^3.2.0", - "mlly": "^1.1.1", + "mlly": "^1.2.0", "pathe": "^1.1.0" } }, @@ -15452,9 +15301,9 @@ } }, "regenerator-runtime": { - "version": "0.13.10", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz", - "integrity": "sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw==" + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, "regexp.prototype.flags": { "version": "1.4.3", @@ -15680,12 +15529,6 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, "source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", @@ -16035,9 +15878,9 @@ "dev": true }, "tinypool": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.4.0.tgz", - "integrity": "sha512-2ksntHOKf893wSAH4z/+JbPpi92esw8Gn9N2deXX+B0EO92hexAVI9GIZZPx7P5aYo5KULfeOSt3kMOmSOy6uA==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.5.0.tgz", + "integrity": "sha512-paHQtnrlS1QZYKF/GnLoOM/DN9fqaGOFbCbxzAhwniySnzl9Ebk8w73/dd34DAhe/obUbPAOldTyYXQZxnPBPQ==", "dev": true }, "tinyspy": { @@ -16209,9 +16052,9 @@ } }, "ufo": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.1.1.tgz", - "integrity": "sha512-MvlCc4GHrmZdAllBc0iUDowff36Q9Ndw/UzqmEKyrfSzokTd9ZCy1i+IIk5hrYKkjoYVQyNbrw7/F8XJ2rEwTg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.1.2.tgz", + "integrity": "sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==", "dev": true }, "unbox-primitive": { @@ -16348,21 +16191,21 @@ } }, "vite": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.2.tgz", - "integrity": "sha512-9R53Mf+TBoXCYejcL+qFbZde+eZveQLDYd9XgULILLC1a5ZwPaqgmdVpL8/uvw2BM/1TzetWjglwm+3RO+xTyw==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.5.tgz", + "integrity": "sha512-0gEnL9wiRFxgz40o/i/eTBwm+NEbpUeTWhzKrZDSdKm6nplj+z4lKz8ANDgildxHm47Vg8EUia0aicKbawUVVA==", "dev": true, "requires": { "esbuild": "^0.17.5", "fsevents": "~2.3.2", - "postcss": "^8.4.21", + "postcss": "^8.4.23", "rollup": "^3.21.0" } }, "vite-node": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.30.1.tgz", - "integrity": "sha512-vTikpU/J7e6LU/8iM3dzBo8ZhEiKZEKRznEMm+mJh95XhWaPrJQraT/QsT2NWmuEf+zgAoMe64PKT7hfZ1Njmg==", + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.31.0.tgz", + "integrity": "sha512-8x1x1LNuPvE2vIvkSB7c1mApX5oqlgsxzHQesYF7l5n1gKrEmrClIiZuOFbFDQcjLsmcWSwwmrWrcGWm9Fxc/g==", "dev": true, "requires": { "cac": "^6.7.14", @@ -16380,19 +16223,19 @@ "requires": {} }, "vitest": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.30.1.tgz", - "integrity": "sha512-y35WTrSTlTxfMLttgQk4rHcaDkbHQwDP++SNwPb+7H8yb13Q3cu2EixrtHzF27iZ8v0XCciSsLg00RkPAzB/aA==", + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.31.0.tgz", + "integrity": "sha512-JwWJS9p3GU9GxkG7eBSmr4Q4x4bvVBSswaCFf1PBNHiPx00obfhHRJfgHcnI0ffn+NMlIh9QGvG75FlaIBdKGA==", "dev": true, "requires": { "@types/chai": "^4.3.4", "@types/chai-subset": "^1.3.3", "@types/node": "*", - "@vitest/expect": "0.30.1", - "@vitest/runner": "0.30.1", - "@vitest/snapshot": "0.30.1", - "@vitest/spy": "0.30.1", - "@vitest/utils": "0.30.1", + "@vitest/expect": "0.31.0", + "@vitest/runner": "0.31.0", + "@vitest/snapshot": "0.31.0", + "@vitest/spy": "0.31.0", + "@vitest/utils": "0.31.0", "acorn": "^8.8.2", "acorn-walk": "^8.2.0", "cac": "^6.7.14", @@ -16403,13 +16246,12 @@ "magic-string": "^0.30.0", "pathe": "^1.1.0", "picocolors": "^1.0.0", - "source-map": "^0.6.1", "std-env": "^3.3.2", "strip-literal": "^1.0.1", "tinybench": "^2.4.0", - "tinypool": "^0.4.0", + "tinypool": "^0.5.0", "vite": "^3.0.0 || ^4.0.0", - "vite-node": "0.30.1", + "vite-node": "0.31.0", "why-is-node-running": "^2.2.2" } }, diff --git a/web/package.json b/web/package.json index cd32ce888..98655fedb 100644 --- a/web/package.json +++ b/web/package.json @@ -13,11 +13,11 @@ }, "dependencies": { "@cycjimmy/jsmpeg-player": "^6.0.5", - "axios": "^1.3.6", + "axios": "^1.4.0", "copy-to-clipboard": "3.3.3", - "date-fns": "^2.29.3", + "date-fns": "^2.30.0", "idb-keyval": "^6.2.0", - "immer": "^9.0.21", + "immer": "^10.0.1", "monaco-yaml": "^4.0.4", "preact": "^10.13.2", "preact-async-route": "^2.2.1", @@ -38,21 +38,21 @@ "@testing-library/user-event": "^14.4.3", "@typescript-eslint/eslint-plugin": "^5.59.1", "@typescript-eslint/parser": "^5.59.1", - "@vitest/coverage-c8": "^0.30.1", - "@vitest/ui": "^0.30.1", + "@vitest/coverage-c8": "^0.31.0", + "@vitest/ui": "^0.31.0", "autoprefixer": "^10.4.14", "eslint": "^8.39.0", "eslint-config-preact": "^1.3.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-vitest-globals": "^1.3.1", "fake-indexeddb": "^4.0.1", - "jsdom": "^21.1.1", + "jsdom": "^22.0.0", "msw": "^1.2.1", "postcss": "^8.4.23", "prettier": "^2.8.8", "tailwindcss": "^3.3.2", "typescript": "^5.0.4", - "vite": "^4.3.2", - "vitest": "^0.30.1" + "vite": "^4.3.5", + "vitest": "^0.31.0" } } diff --git a/web/src/api/ws.jsx b/web/src/api/ws.jsx index 8995a065b..0867ed0a4 100644 --- a/web/src/api/ws.jsx +++ b/web/src/api/ws.jsx @@ -1,6 +1,6 @@ import { h, createContext } from 'preact'; import { baseUrl } from './baseUrl'; -import produce from 'immer'; +import { produce } from 'immer'; import { useCallback, useContext, useEffect, useRef, useReducer } from 'preact/hooks'; const initialState = Object.freeze({ __connected: false });