diff --git a/docker/main/rootfs/usr/local/go2rtc/create_config.py b/docker/main/rootfs/usr/local/go2rtc/create_config.py index 77cfc1b880e..70cb744f135 100644 --- a/docker/main/rootfs/usr/local/go2rtc/create_config.py +++ b/docker/main/rootfs/usr/local/go2rtc/create_config.py @@ -3,6 +3,7 @@ import json import os import sys +from pathlib import Path from typing import Any from ruamel.yaml import YAML diff --git a/docs/docs/integrations/mqtt.md b/docs/docs/integrations/mqtt.md index 44d6ecee0ba..730f20259d9 100644 --- a/docs/docs/integrations/mqtt.md +++ b/docs/docs/integrations/mqtt.md @@ -185,7 +185,8 @@ Message published when [object classification](/configuration/custom_classificat "timestamp": 1607123958.748393, "model": "person_classifier", "sub_label": "delivery_person", - "score": 0.87 + "score": 0.87, + "zones": ["front_yard", "driveway"] } ``` @@ -199,10 +200,15 @@ Message published when [object classification](/configuration/custom_classificat "timestamp": 1607123958.748393, "model": "helmet_detector", "attribute": "yes", - "score": 0.92 + "score": 0.92, + "zones": ["front_yard"] } ``` +:::note +The `zones` field is only included if the tracked object is currently in one or more zones. +::: + ### `frigate/reviews` Message published for each changed review item. The first message is published when the `detection` or `alert` is initiated. diff --git a/docs/static/frigate-api.yaml b/docs/static/frigate-api.yaml index af3a019b4c4..766db5df8e6 100644 --- a/docs/static/frigate-api.yaml +++ b/docs/static/frigate-api.yaml @@ -1654,7 +1654,8 @@ paths: **Access:** Admin role required. Categorizes a specific classification image for a given classification model and category. - The image must exist in the specified category. Returns a success message or an error if the name or category is invalid. + Accepts either a training file from the train directory or an event_id to extract + the object crop from. Returns a success message or an error if the name or category is invalid. operationId: categorize_classification_image_classification__name__dataset_categorize_post parameters: diff --git a/frigate/api/camera.py b/frigate/api/camera.py index 0b01ada1d52..dd24c0a9e7e 100644 --- a/frigate/api/camera.py +++ b/frigate/api/camera.py @@ -647,7 +647,9 @@ async def _connect_onvif_camera( return onvif_camera # Both encodings failed authentication; surface the original fault. - raise first_error + if first_error is not None: + raise first_error + raise ONVIFError("Failed to connect to ONVIF device with both password encodings") @router.get( diff --git a/frigate/api/classification.py b/frigate/api/classification.py index 6dd7055092e..e6b2f8ec893 100644 --- a/frigate/api/classification.py +++ b/frigate/api/classification.py @@ -1061,7 +1061,8 @@ def rename_classification_category( dependencies=[Depends(require_role(["admin"]))], summary="Categorize a classification image", description="""Categorizes a specific classification image for a given classification model and category. - The image must exist in the specified category. Returns a success message or an error if the name or category is invalid.""", + Accepts either a training file from the train directory or an event_id to extract + the object crop from. Returns a success message or an error if the name or category is invalid.""", ) def categorize_classification_image(request: Request, name: str, body: dict = None): config: FrigateConfig = request.app.frigate_config @@ -1080,19 +1081,17 @@ def categorize_classification_image(request: Request, name: str, body: dict = No json: dict[str, Any] = body or {} category = sanitize_filename(json.get("category", "")) training_file_name = sanitize_filename(json.get("training_file", "")) - training_file = os.path.join( - CLIPS_DIR, sanitize_filename(name), "train", training_file_name - ) + event_id = json.get("event_id") - if training_file_name and not os.path.isfile(training_file): + if not training_file_name and not event_id: return JSONResponse( content=( { "success": False, - "message": f"Invalid filename or no file exists: {training_file_name}", + "message": "A training file or event_id must be passed.", } ), - status_code=404, + status_code=400, ) random_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6)) @@ -1104,10 +1103,116 @@ def categorize_classification_image(request: Request, name: str, body: dict = No os.makedirs(new_file_folder, exist_ok=True) - # use opencv because webp images can not be used to train - img = cv2.imread(training_file) - cv2.imwrite(os.path.join(new_file_folder, new_name), img) - os.unlink(training_file) + if training_file_name: + # Use existing training file + training_file = os.path.join( + CLIPS_DIR, sanitize_filename(name), "train", training_file_name + ) + + if not os.path.isfile(training_file): + return JSONResponse( + content=( + { + "success": False, + "message": f"Invalid filename or no file exists: {training_file_name}", + } + ), + status_code=404, + ) + + # use opencv because webp images can not be used to train + img = cv2.imread(training_file) + cv2.imwrite(os.path.join(new_file_folder, new_name), img) + os.unlink(training_file) + else: + # Extract from event + try: + event: Event = Event.get(Event.id == event_id) + except DoesNotExist: + return JSONResponse( + content=( + { + "success": False, + "message": f"Invalid event_id or no event exists: {event_id}", + } + ), + status_code=404, + ) + + snapshot = get_event_snapshot(event) + + if snapshot is None: + return JSONResponse( + content=( + { + "success": False, + "message": f"Failed to read snapshot for event {event_id}.", + } + ), + status_code=500, + ) + + # Get object bounding box for the first detection + if not event.data.get("attributes") or len(event.data["attributes"]) == 0: + return JSONResponse( + content=( + { + "success": False, + "message": f"Event {event_id} has no detection attributes.", + } + ), + status_code=400, + ) + + # Use the first attribute's box + box = event.data["attributes"][0]["box"] + + try: + # Extract the crop from the snapshot + frame = snapshot + + height, width = frame.shape[:2] + + # Convert relative coordinates to absolute + x1 = int(box[0] * width) + y1 = int(box[1] * height) + x2 = int(box[2] * width) + y2 = int(box[3] * height) + + # Ensure coordinates are within frame boundaries + x1 = max(0, x1) + y1 = max(0, y1) + x2 = min(width, x2) + y2 = min(height, y2) + + # Extract the crop + crop = frame[y1:y2, x1:x2] + + if crop.size == 0: + return JSONResponse( + content=( + { + "success": False, + "message": f"Failed to extract crop from event {event_id}.", + } + ), + status_code=500, + ) + + # Save the crop + cv2.imwrite(os.path.join(new_file_folder, new_name), crop) + + except Exception as e: + logger.error(f"Failed to extract classification crop: {e}") + return JSONResponse( + content=( + { + "success": False, + "message": f"Failed to process event {event_id}: {str(e)}", + } + ), + status_code=500, + ) return JSONResponse( content=({"success": True, "message": "Successfully categorized image."}), diff --git a/frigate/camera/activity_manager.py b/frigate/camera/activity_manager.py index 38425add9a5..dcda96a9687 100644 --- a/frigate/camera/activity_manager.py +++ b/frigate/camera/activity_manager.py @@ -26,8 +26,16 @@ def __init__( self.last_camera_activity: dict[str, dict[str, Any]] = {} self.camera_all_object_counts: dict[str, Counter] = {} self.camera_active_object_counts: dict[str, Counter] = {} + self.camera_all_label_counts: dict[str, Counter] = {} + self.camera_active_label_counts: dict[str, Counter] = {} + self.camera_all_attribute_counts: dict[str, Counter] = {} + self.camera_active_attribute_counts: dict[str, Counter] = {} self.zone_all_object_counts: dict[str, Counter] = {} self.zone_active_object_counts: dict[str, Counter] = {} + self.zone_all_label_counts: dict[str, Counter] = {} + self.zone_active_label_counts: dict[str, Counter] = {} + self.zone_all_attribute_counts: dict[str, Counter] = {} + self.zone_active_attribute_counts: dict[str, Counter] = {} self.all_zone_labels: dict[str, set[str]] = {} for camera_config in config.cameras.values(): @@ -43,11 +51,19 @@ def __init_camera(self, camera_config: CameraConfig) -> None: self.last_camera_activity[camera_config.name] = {} self.camera_all_object_counts[camera_config.name] = Counter() self.camera_active_object_counts[camera_config.name] = Counter() + self.camera_all_label_counts[camera_config.name] = Counter() + self.camera_active_label_counts[camera_config.name] = Counter() + self.camera_all_attribute_counts[camera_config.name] = Counter() + self.camera_active_attribute_counts[camera_config.name] = Counter() for zone, zone_config in camera_config.zones.items(): if zone not in self.all_zone_labels: self.zone_all_object_counts[zone] = Counter() self.zone_active_object_counts[zone] = Counter() + self.zone_all_label_counts[zone] = Counter() + self.zone_active_label_counts[zone] = Counter() + self.zone_all_attribute_counts[zone] = Counter() + self.zone_active_attribute_counts[zone] = Counter() self.all_zone_labels[zone] = set() self.all_zone_labels[zone].update( @@ -75,15 +91,21 @@ def update_activity(self, new_activity: dict[str, dict[str, Any]]) -> None: # run through every zone, getting a count of objects in that zone right now for zone, labels in self.all_zone_labels.items(): + zone_objects_by_id = { + obj["id"]: obj for obj in all_objects if zone in obj["current_zones"] + } all_zone_objects = Counter( obj["label"].replace("-verified", "") - for obj in all_objects - if zone in obj["current_zones"] + for obj in zone_objects_by_id.values() ) - active_zone_objects = Counter( - obj["label"].replace("-verified", "") + active_zone_objects_by_id = { + obj["id"]: obj for obj in all_objects if zone in obj["current_zones"] and not obj["stationary"] + } + active_zone_objects = Counter( + obj["label"].replace("-verified", "") + for obj in active_zone_objects_by_id.values() ) any_changed = False @@ -108,6 +130,86 @@ def update_activity(self, new_activity: dict[str, dict[str, Any]]) -> None: self.publish(f"{zone}/{label}/active", new_active_count) self.zone_active_object_counts[zone][label] = new_active_count + # Compute custom classification (sub_label) counts per object type + all_zone_label_counts = Counter( + (obj["object_type"], obj["sub_label"]) + for obj in zone_objects_by_id.values() + if obj.get("sub_label") + ) + active_zone_label_counts = Counter( + (obj["object_type"], obj["sub_label"]) + for obj in active_zone_objects_by_id.values() + if obj.get("sub_label") + ) + + # Publish sub-label (custom classification) counts + for key in set(all_zone_label_counts) | set( + self.zone_all_label_counts[zone] + ): + object_type, sub_label = key + new_count = all_zone_label_counts[key] + new_active = active_zone_label_counts[key] + + if ( + new_count != self.zone_all_label_counts[zone][key] + or key not in self.zone_all_label_counts[zone] + ): + any_changed = True + self.publish(f"{zone}/{object_type}/label/{sub_label}", new_count) + self.zone_all_label_counts[zone][key] = new_count + + if ( + new_active != self.zone_active_label_counts[zone][key] + or key not in self.zone_active_label_counts[zone] + ): + any_changed = True + self.publish( + f"{zone}/{object_type}/label/{sub_label}/active", new_active + ) + self.zone_active_label_counts[zone][key] = new_active + + # Compute attribute counts per object type + # Attributes are objects whose label differs from object_type and have no sub_label + all_zone_attribute_counts = Counter( + (obj["object_type"], obj["label"]) + for obj in zone_objects_by_id.values() + if not obj.get("sub_label") and obj["object_type"] != obj["label"] + ) + active_zone_attribute_counts = Counter( + (obj["object_type"], obj["label"]) + for obj in active_zone_objects_by_id.values() + if not obj.get("sub_label") and obj["object_type"] != obj["label"] + ) + + # Publish attribute counts + for key in set(all_zone_attribute_counts) | set( + self.zone_all_attribute_counts[zone] + ): + object_type, attribute = key + new_count = all_zone_attribute_counts[key] + new_active = active_zone_attribute_counts[key] + + if ( + new_count != self.zone_all_attribute_counts[zone][key] + or key not in self.zone_all_attribute_counts[zone] + ): + any_changed = True + self.publish( + f"{zone}/{object_type}/attribute/{attribute}", new_count + ) + self.zone_all_attribute_counts[zone][key] = new_count + + if ( + new_active != self.zone_active_attribute_counts[zone][key] + or key not in self.zone_active_attribute_counts[zone] + ): + any_changed = True + self.publish( + f"{zone}/{object_type}/attribute/{attribute}/active", + new_active, + ) + self.zone_active_attribute_counts[zone][key] = new_active + if any_changed: self.publish(f"{zone}/all", sum(list(all_zone_objects.values()))) self.publish( @@ -119,13 +221,16 @@ def update_activity(self, new_activity: dict[str, dict[str, Any]]) -> None: def compare_camera_activity( self, camera: str, new_activity: list[dict[str, Any]] ) -> None: + objects_by_id = {obj["id"]: obj for obj in new_activity} all_objects = Counter( - obj["label"].replace("-verified", "") for obj in new_activity + obj["label"].replace("-verified", "") for obj in objects_by_id.values() ) + active_objects_by_id = { + obj["id"]: obj for obj in new_activity if not obj["stationary"] + } active_objects = Counter( obj["label"].replace("-verified", "") - for obj in new_activity - if not obj["stationary"] + for obj in active_objects_by_id.values() ) any_changed = False @@ -157,6 +262,84 @@ def compare_camera_activity( self.publish(f"{camera}/{label}/active", new_active_count) self.camera_active_object_counts[camera][label] = new_active_count + # Compute custom classification (sub_label) counts per object type + all_camera_label_counts = Counter( + (obj["object_type"], obj["sub_label"]) + for obj in objects_by_id.values() + if obj.get("sub_label") + ) + active_camera_label_counts = Counter( + (obj["object_type"], obj["sub_label"]) + for obj in active_objects_by_id.values() + if obj.get("sub_label") + ) + + # Publish sub-label (custom classification) counts + for key in set(all_camera_label_counts) | set( + self.camera_all_label_counts[camera] + ): + object_type, sub_label = key + new_count = all_camera_label_counts[key] + new_active = active_camera_label_counts[key] + + if ( + new_count != self.camera_all_label_counts[camera][key] + or key not in self.camera_all_label_counts[camera] + ): + any_changed = True + self.publish(f"{camera}/{object_type}/label/{sub_label}", new_count) + self.camera_all_label_counts[camera][key] = new_count + + if ( + new_active != self.camera_active_label_counts[camera][key] + or key not in self.camera_active_label_counts[camera] + ): + any_changed = True + self.publish( + f"{camera}/{object_type}/label/{sub_label}/active", new_active + ) + self.camera_active_label_counts[camera][key] = new_active + + # Compute attribute counts per object type + # Attributes are objects whose label differs from object_type and have no sub_label + all_camera_attribute_counts = Counter( + (obj["object_type"], obj["label"]) + for obj in objects_by_id.values() + if not obj.get("sub_label") and obj["object_type"] != obj["label"] + ) + active_camera_attribute_counts = Counter( + (obj["object_type"], obj["label"]) + for obj in active_objects_by_id.values() + if not obj.get("sub_label") and obj["object_type"] != obj["label"] + ) + + # Publish attribute counts + for key in set(all_camera_attribute_counts) | set( + self.camera_all_attribute_counts[camera] + ): + object_type, attribute = key + new_count = all_camera_attribute_counts[key] + new_active = active_camera_attribute_counts[key] + + if ( + new_count != self.camera_all_attribute_counts[camera][key] + or key not in self.camera_all_attribute_counts[camera] + ): + any_changed = True + self.publish(f"{camera}/{object_type}/attribute/{attribute}", new_count) + self.camera_all_attribute_counts[camera][key] = new_count + + if ( + new_active != self.camera_active_attribute_counts[camera][key] + or key not in self.camera_active_attribute_counts[camera] + ): + any_changed = True + self.publish( + f"{camera}/{object_type}/attribute/{attribute}/active", + new_active, + ) + self.camera_active_attribute_counts[camera][key] = new_active + if any_changed: self.publish(f"{camera}/all", sum(list(all_objects.values()))) self.publish(f"{camera}/all/active", sum(list(active_objects.values()))) diff --git a/frigate/camera/state.py b/frigate/camera/state.py index f35a3eaa56d..f32af4f0142 100644 --- a/frigate/camera/state.py +++ b/frigate/camera/state.py @@ -518,6 +518,7 @@ def update( camera_activity["objects"].append( { "id": obj.obj_data["id"], + "object_type": object_type, "label": label, "stationary": not active, "area": obj.obj_data["area"], diff --git a/frigate/data_processing/real_time/custom_classification.py b/frigate/data_processing/real_time/custom_classification.py index e3b0e23ed81..b07d7a36830 100644 --- a/frigate/data_processing/real_time/custom_classification.py +++ b/frigate/data_processing/real_time/custom_classification.py @@ -573,14 +573,26 @@ def process_frame(self, obj_data: dict[str, Any], frame: np.ndarray) -> None: crop_bgr = cv2.cvtColor(crop, cv2.COLOR_RGB2BGR) self._enqueue_task( - ("classify", object_id, obj_data["camera"], now, resized_crop, crop_bgr) + ( + "classify", + object_id, + obj_data["camera"], + now, + resized_crop, + crop_bgr, + obj_data.get("current_zones", []), + ) ) def _process_task(self, task: Any) -> None: kind = task[0] if kind == "classify": - _, object_id, camera, timestamp, resized_crop, crop_bgr = task - self._classify_object(object_id, camera, timestamp, resized_crop, crop_bgr) + _, object_id, camera, timestamp, resized_crop, crop_bgr, current_zones = ( + task + ) + self._classify_object( + object_id, camera, timestamp, resized_crop, crop_bgr, current_zones + ) elif kind == "expire": _, object_id = task if object_id in self.classification_history: @@ -595,6 +607,7 @@ def _classify_object( timestamp: float, resized_crop: np.ndarray, crop_bgr: np.ndarray, + current_zones: list[str] | None = None, ) -> None: if self.interpreter is None: save_attempts = ( @@ -673,19 +686,20 @@ def _classify_object( ) if consensus_label is not None and self.model_config.object_config is not None: - self._emit_result( - { - "type": "classification", - "processor": "object", - "model_name": self.model_config.name, - "classification_type": self.model_config.object_config.classification_type, - "object_id": object_id, - "camera": camera, - "timestamp": timestamp, - "label": consensus_label, - "score": consensus_score, - } - ) + result: dict[str, Any] = { + "type": "classification", + "processor": "object", + "model_name": self.model_config.name, + "classification_type": self.model_config.object_config.classification_type, + "object_id": object_id, + "camera": camera, + "timestamp": timestamp, + "label": consensus_label, + "score": consensus_score, + } + if current_zones: + result["zones"] = current_zones + self._emit_result(result) def handle_request( self, topic: str, request_data: dict[str, Any] diff --git a/frigate/embeddings/maintainer.py b/frigate/embeddings/maintainer.py index 52bdf5d9152..818f68f5c56 100644 --- a/frigate/embeddings/maintainer.py +++ b/frigate/embeddings/maintainer.py @@ -756,44 +756,47 @@ def _process_deferred_results(self) -> None: label = result["label"] score = result["score"] classification_type = result["classification_type"] + zones = result.get("zones") if classification_type == ObjectClassificationType.sub_label: self.event_metadata_publisher.publish( (object_id, label, score), EventMetadataTypeEnum.sub_label, ) + payload: dict[str, Any] = { + "type": TrackedObjectUpdateTypesEnum.classification, + "id": object_id, + "camera": camera, + "timestamp": timestamp, + "model": model_name, + "sub_label": label, + "score": score, + } + if zones: + payload["zones"] = zones self.requestor.send_data( "tracked_object_update", - json.dumps( - { - "type": TrackedObjectUpdateTypesEnum.classification, - "id": object_id, - "camera": camera, - "timestamp": timestamp, - "model": model_name, - "sub_label": label, - "score": score, - } - ), + json.dumps(payload), ) elif classification_type == ObjectClassificationType.attribute: self.event_metadata_publisher.publish( (object_id, model_name, label, score), EventMetadataTypeEnum.attribute.value, ) + payload = { + "type": TrackedObjectUpdateTypesEnum.classification, + "id": object_id, + "camera": camera, + "timestamp": timestamp, + "model": model_name, + "attribute": label, + "score": score, + } + if zones: + payload["zones"] = zones self.requestor.send_data( "tracked_object_update", - json.dumps( - { - "type": TrackedObjectUpdateTypesEnum.classification, - "id": object_id, - "camera": camera, - "timestamp": timestamp, - "model": model_name, - "attribute": label, - "score": score, - } - ), + json.dumps(payload), ) def _embed_thumbnail(self, event_id: str, thumbnail: bytes) -> None: diff --git a/frigate/test/test_activity_manager.py b/frigate/test/test_activity_manager.py new file mode 100644 index 00000000000..9676306fb13 --- /dev/null +++ b/frigate/test/test_activity_manager.py @@ -0,0 +1,627 @@ +"""Tests for CameraActivityManager zone label and attribute MQTT publishing.""" + +import sys +import unittest +from unittest.mock import MagicMock + +# Save the original sys.modules entries so we can restore them after the import. +# Without this, the mocks would leak into every other test module that imports +# these packages after this file is discovered, corrupting their imports. +_MOCKED_MODS = [ + "zmq", + "frigate.comms.zmq_proxy", + "frigate.comms.event_metadata_updater", + "frigate.config", +] +_saved_modules = {mod: sys.modules.get(mod) for mod in _MOCKED_MODS} + +# Mock all modules that have native/missing dependencies before any imports +for _mod in _MOCKED_MODS: + sys.modules[_mod] = MagicMock() + +# Provide real-looking CameraConfig / FrigateConfig stubs so the type hints work +_config_mod = sys.modules["frigate.config"] +_config_mod.CameraConfig = MagicMock +_config_mod.FrigateConfig = MagicMock + +from frigate.camera.activity_manager import CameraActivityManager # noqa: E402 + +# Restore sys.modules immediately so subsequent test-module imports get the +# real packages, not these mocks. +for _mod, _original in _saved_modules.items(): + if _original is None: + sys.modules.pop(_mod, None) + else: + sys.modules[_mod] = _original +del _mod, _original, _saved_modules, _config_mod, _MOCKED_MODS + + +def _make_config(zone_name="driveway", zone_objects=None, track_objects=None): + """Build a minimal mock FrigateConfig with one camera and one zone.""" + if zone_objects is None: + zone_objects = ["person", "car"] + if track_objects is None: + track_objects = ["person", "car"] + + zone_config = MagicMock() + zone_config.objects = zone_objects + + camera_config = MagicMock() + camera_config.name = "front" + camera_config.enabled_in_config = True + camera_config.zones = {zone_name: zone_config} + camera_config.objects.track = track_objects + + config = MagicMock() + config.cameras = {"front": camera_config} + config.model.non_logo_attributes = ["face", "license_plate"] + + return config + + +def _make_object( + obj_id, + object_type, + label, + sub_label=None, + stationary=False, + current_zones=None, +): + """Build a minimal activity object dict matching the structure from state.py.""" + return { + "id": obj_id, + "object_type": object_type, + "label": label, + "stationary": stationary, + "area": 10000, + "ratio": 1.0, + "score": 0.9, + "sub_label": sub_label, + "current_zones": current_zones or [], + } + + +class TestZoneLabelPublishing(unittest.TestCase): + """Tests that custom classification sub-label counts are published to + {zone}/{object_type}/label/{sub_label} and {zone}/{object_type}/label/{sub_label}/active. + """ + + def setUp(self): + self.publish = MagicMock() + self.manager = CameraActivityManager(_make_config(), self.publish) + + def test_sub_label_total_published(self): + """Publish {zone}/{object_type}/label/{sub_label} for a classified object.""" + activity = { + "front": { + "motion": False, + "objects": [ + _make_object( + "obj1", + "person", + "person-verified", + sub_label="running", + stationary=True, + current_zones=["driveway"], + ) + ], + } + } + self.manager.update_activity(activity) + + self.publish.assert_any_call("driveway/person/label/running", 1) + + def test_sub_label_active_published(self): + """Publish {zone}/{object_type}/label/{sub_label}/active for active classified object.""" + activity = { + "front": { + "motion": False, + "objects": [ + _make_object( + "obj1", + "person", + "person-verified", + sub_label="running", + stationary=False, + current_zones=["driveway"], + ) + ], + } + } + self.manager.update_activity(activity) + + self.publish.assert_any_call("driveway/person/label/running", 1) + self.publish.assert_any_call("driveway/person/label/running/active", 1) + + def test_sub_label_active_zero_when_stationary(self): + """Active count is 0 when classified object is stationary.""" + activity = { + "front": { + "motion": False, + "objects": [ + _make_object( + "obj1", + "person", + "person-verified", + sub_label="walking", + stationary=True, + current_zones=["driveway"], + ) + ], + } + } + self.manager.update_activity(activity) + + self.publish.assert_any_call("driveway/person/label/walking", 1) + self.publish.assert_any_call("driveway/person/label/walking/active", 0) + + def test_sub_label_count_published_zero_when_object_leaves(self): + """Count drops to 0 and is published when a classified object leaves the zone.""" + activity_with = { + "front": { + "motion": False, + "objects": [ + _make_object( + "obj1", + "person", + "person-verified", + sub_label="running", + stationary=False, + current_zones=["driveway"], + ) + ], + } + } + activity_without = {"front": {"motion": False, "objects": []}} + + self.manager.update_activity(activity_with) + self.publish.reset_mock() + self.manager.update_activity(activity_without) + + self.publish.assert_any_call("driveway/person/label/running", 0) + self.publish.assert_any_call("driveway/person/label/running/active", 0) + + def test_multiple_sub_labels_counted_separately(self): + """Different sub_labels under the same object_type are counted separately.""" + activity = { + "front": { + "motion": False, + "objects": [ + _make_object( + "obj1", + "person", + "person-verified", + sub_label="running", + stationary=False, + current_zones=["driveway"], + ), + _make_object( + "obj2", + "person", + "person-verified", + sub_label="sitting", + stationary=True, + current_zones=["driveway"], + ), + ], + } + } + self.manager.update_activity(activity) + + self.publish.assert_any_call("driveway/person/label/running", 1) + self.publish.assert_any_call("driveway/person/label/running/active", 1) + self.publish.assert_any_call("driveway/person/label/sitting", 1) + self.publish.assert_any_call("driveway/person/label/sitting/active", 0) + + def test_sub_label_not_published_for_object_outside_zone(self): + """Sub-label counts are not published for objects outside the zone.""" + activity = { + "front": { + "motion": False, + "objects": [ + _make_object( + "obj1", + "person", + "person-verified", + sub_label="running", + stationary=False, + current_zones=[], # not in any zone + ) + ], + } + } + self.manager.update_activity(activity) + + published_topics = [c.args[0] for c in self.publish.call_args_list] + self.assertNotIn("driveway/person/label/running", published_topics) + self.assertNotIn("driveway/person/label/running/active", published_topics) + + +class TestZoneAttributePublishing(unittest.TestCase): + """Tests that attribute counts are published to + {zone}/{object_type}/attribute/{attribute} and + {zone}/{object_type}/attribute/{attribute}/active. + """ + + def setUp(self): + self.publish = MagicMock() + self.manager = CameraActivityManager(_make_config(), self.publish) + + def test_attribute_total_published(self): + """Publish {zone}/{object_type}/attribute/{attribute} for an attribute object.""" + # Attributes: label IS the attribute (e.g. "amazon"), object_type is the parent ("car") + activity = { + "front": { + "motion": False, + "objects": [ + _make_object( + "obj1", + "car", + "amazon", + sub_label=None, + stationary=True, + current_zones=["driveway"], + ) + ], + } + } + self.manager.update_activity(activity) + + self.publish.assert_any_call("driveway/car/attribute/amazon", 1) + + def test_attribute_active_published(self): + """Publish {zone}/{object_type}/attribute/{attribute}/active for active attribute object.""" + activity = { + "front": { + "motion": False, + "objects": [ + _make_object( + "obj1", + "car", + "amazon", + sub_label=None, + stationary=False, + current_zones=["driveway"], + ) + ], + } + } + self.manager.update_activity(activity) + + self.publish.assert_any_call("driveway/car/attribute/amazon", 1) + self.publish.assert_any_call("driveway/car/attribute/amazon/active", 1) + + def test_attribute_active_zero_when_stationary(self): + """Active count is 0 when attribute object is stationary.""" + activity = { + "front": { + "motion": False, + "objects": [ + _make_object( + "obj1", + "car", + "license_plate", + sub_label=None, + stationary=True, + current_zones=["driveway"], + ) + ], + } + } + self.manager.update_activity(activity) + + self.publish.assert_any_call("driveway/car/attribute/license_plate", 1) + self.publish.assert_any_call("driveway/car/attribute/license_plate/active", 0) + + def test_attribute_count_drops_to_zero_when_object_leaves(self): + """Count drops to 0 and is published when an attribute object leaves the zone.""" + activity_with = { + "front": { + "motion": False, + "objects": [ + _make_object( + "obj1", + "car", + "amazon", + sub_label=None, + stationary=False, + current_zones=["driveway"], + ) + ], + } + } + activity_without = {"front": {"motion": False, "objects": []}} + + self.manager.update_activity(activity_with) + self.publish.reset_mock() + self.manager.update_activity(activity_without) + + self.publish.assert_any_call("driveway/car/attribute/amazon", 0) + self.publish.assert_any_call("driveway/car/attribute/amazon/active", 0) + + def test_base_object_not_treated_as_attribute(self): + """A plain object without sub_label whose label == object_type is NOT published as attribute.""" + activity = { + "front": { + "motion": False, + "objects": [ + _make_object( + "obj1", + "person", + "person", + sub_label=None, + stationary=False, + current_zones=["driveway"], + ) + ], + } + } + self.manager.update_activity(activity) + + published_topics = [c.args[0] for c in self.publish.call_args_list] + attribute_topics = [t for t in published_topics if "/attribute/" in t] + self.assertEqual([], attribute_topics) + + +class TestCameraLabelPublishing(unittest.TestCase): + """Tests that custom classification sub-label counts are published to + {camera}/{object_type}/label/{sub_label} and {camera}/{object_type}/label/{sub_label}/active. + """ + + def setUp(self): + self.publish = MagicMock() + self.manager = CameraActivityManager(_make_config(), self.publish) + + def test_sub_label_total_published_for_camera(self): + """Publish {camera}/{object_type}/label/{sub_label} for a classified object.""" + activity = { + "front": { + "motion": False, + "objects": [ + _make_object( + "obj1", + "person", + "person-verified", + sub_label="running", + stationary=True, + current_zones=[], + ) + ], + } + } + self.manager.update_activity(activity) + + self.publish.assert_any_call("front/person/label/running", 1) + + def test_sub_label_active_published_for_camera(self): + """Publish {camera}/{object_type}/label/{sub_label}/active for active classified object.""" + activity = { + "front": { + "motion": False, + "objects": [ + _make_object( + "obj1", + "person", + "person-verified", + sub_label="running", + stationary=False, + current_zones=[], + ) + ], + } + } + self.manager.update_activity(activity) + + self.publish.assert_any_call("front/person/label/running", 1) + self.publish.assert_any_call("front/person/label/running/active", 1) + + def test_sub_label_active_zero_when_stationary_for_camera(self): + """Active count is 0 when classified object is stationary on a camera.""" + activity = { + "front": { + "motion": False, + "objects": [ + _make_object( + "obj1", + "person", + "person-verified", + sub_label="walking", + stationary=True, + current_zones=[], + ) + ], + } + } + self.manager.update_activity(activity) + + self.publish.assert_any_call("front/person/label/walking", 1) + self.publish.assert_any_call("front/person/label/walking/active", 0) + + def test_sub_label_count_drops_to_zero_for_camera(self): + """Count drops to 0 and is published when a classified object leaves the camera.""" + activity_with = { + "front": { + "motion": False, + "objects": [ + _make_object( + "obj1", + "person", + "person-verified", + sub_label="running", + stationary=False, + current_zones=[], + ) + ], + } + } + activity_without = {"front": {"motion": False, "objects": []}} + + self.manager.update_activity(activity_with) + self.publish.reset_mock() + self.manager.update_activity(activity_without) + + self.publish.assert_any_call("front/person/label/running", 0) + self.publish.assert_any_call("front/person/label/running/active", 0) + + +class TestCameraAttributePublishing(unittest.TestCase): + """Tests that attribute counts are published to + {camera}/{object_type}/attribute/{attribute} and + {camera}/{object_type}/attribute/{attribute}/active. + """ + + def setUp(self): + self.publish = MagicMock() + self.manager = CameraActivityManager(_make_config(), self.publish) + + def test_attribute_total_published_for_camera(self): + """Publish {camera}/{object_type}/attribute/{attribute} for an attribute object.""" + activity = { + "front": { + "motion": False, + "objects": [ + _make_object( + "obj1", + "car", + "amazon", + sub_label=None, + stationary=True, + current_zones=[], + ) + ], + } + } + self.manager.update_activity(activity) + + self.publish.assert_any_call("front/car/attribute/amazon", 1) + + def test_attribute_active_published_for_camera(self): + """Publish {camera}/{object_type}/attribute/{attribute}/active for active attribute object.""" + activity = { + "front": { + "motion": False, + "objects": [ + _make_object( + "obj1", + "car", + "amazon", + sub_label=None, + stationary=False, + current_zones=[], + ) + ], + } + } + self.manager.update_activity(activity) + + self.publish.assert_any_call("front/car/attribute/amazon", 1) + self.publish.assert_any_call("front/car/attribute/amazon/active", 1) + + def test_attribute_active_zero_when_stationary_for_camera(self): + """Active count is 0 when attribute object is stationary on a camera.""" + activity = { + "front": { + "motion": False, + "objects": [ + _make_object( + "obj1", + "car", + "license_plate", + sub_label=None, + stationary=True, + current_zones=[], + ) + ], + } + } + self.manager.update_activity(activity) + + self.publish.assert_any_call("front/car/attribute/license_plate", 1) + self.publish.assert_any_call("front/car/attribute/license_plate/active", 0) + + def test_attribute_count_drops_to_zero_for_camera(self): + """Count drops to 0 and is published when an attribute object leaves the camera.""" + activity_with = { + "front": { + "motion": False, + "objects": [ + _make_object( + "obj1", + "car", + "amazon", + sub_label=None, + stationary=False, + current_zones=[], + ) + ], + } + } + activity_without = {"front": {"motion": False, "objects": []}} + + self.manager.update_activity(activity_with) + self.publish.reset_mock() + self.manager.update_activity(activity_without) + + self.publish.assert_any_call("front/car/attribute/amazon", 0) + self.publish.assert_any_call("front/car/attribute/amazon/active", 0) + + +class TestNoRepublishUnchanged(unittest.TestCase): + """Tests that counts are only re-published when they actually change.""" + + def setUp(self): + self.publish = MagicMock() + self.manager = CameraActivityManager(_make_config(), self.publish) + + def test_no_republish_when_sub_label_count_unchanged(self): + """Sub-label topics are not re-published when the counts haven't changed.""" + activity = { + "front": { + "motion": False, + "objects": [ + _make_object( + "obj1", + "person", + "person-verified", + sub_label="running", + stationary=False, + current_zones=["driveway"], + ) + ], + } + } + self.manager.update_activity(activity) + self.publish.reset_mock() + + # Same activity again — counts are identical, nothing should be re-published + self.manager.update_activity(activity) + + published_topics = [c.args[0] for c in self.publish.call_args_list] + self.assertNotIn("driveway/person/label/running", published_topics) + self.assertNotIn("driveway/person/label/running/active", published_topics) + + def test_no_republish_when_attribute_count_unchanged(self): + """Attribute topics are not re-published when the counts haven't changed.""" + activity = { + "front": { + "motion": False, + "objects": [ + _make_object( + "obj1", + "car", + "amazon", + sub_label=None, + stationary=False, + current_zones=["driveway"], + ) + ], + } + } + self.manager.update_activity(activity) + self.publish.reset_mock() + + self.manager.update_activity(activity) + + published_topics = [c.args[0] for c in self.publish.call_args_list] + self.assertNotIn("driveway/car/attribute/amazon", published_topics) + self.assertNotIn("driveway/car/attribute/amazon/active", published_topics) diff --git a/frigate/test/test_custom_classification.py b/frigate/test/test_custom_classification.py new file mode 100644 index 00000000000..407e96a48be --- /dev/null +++ b/frigate/test/test_custom_classification.py @@ -0,0 +1,361 @@ +import json +import sys +import time +import unittest +from unittest.mock import MagicMock, patch + +# Mock native/optional modules before any Frigate imports so the module can be +# imported in environments where cv2 or TFLite are unavailable (e.g. CI). +_SYS_MOCKS = [ + "cv2", + "tflite_runtime", + "tflite_runtime.interpreter", + "ai_edge_litert", + "ai_edge_litert.interpreter", +] +for _mod in _SYS_MOCKS: + if _mod not in sys.modules: + sys.modules[_mod] = MagicMock() + +WIDTH = 720 +HEIGHT = 1280 + + +class Contains: + def __init__(self, needle): + self.needle = needle + + def __eq__(self, other): + return self.needle in other + + +class TestCustomObjectClassificationZones(unittest.TestCase): + """Test that zone information is correctly added to custom classification MQTT messages""" + + def _build_classification_data( + self, obj_data, classification_type="sub_label", label="person_walking" + ): + """Helper method to build classification data with conditional zones. + + Args: + obj_data: Object data dictionary containing id, camera, and optionally current_zones + classification_type: Either "sub_label" or "attribute" + label: The classification label + + Returns: + Dictionary with classification data, including zones if applicable + """ + classification_data = { + "type": "classification", + "id": obj_data["id"], + "camera": obj_data["camera"], + "timestamp": 1234567890.0, + "model": "test_classifier", + "score": 0.89, + } + + if classification_type == "sub_label": + classification_data["sub_label"] = label + else: + classification_data["attribute"] = label + + if obj_data.get("current_zones"): + classification_data["zones"] = obj_data["current_zones"] + + return classification_data + + def test_sub_label_message_includes_zones_when_present(self): + """Test that zones are included in sub_label classification messages when object is in zones""" + # Create a simple mock requestor + requestor = MagicMock() + + # Create mock obj_data with zones + obj_data = { + "id": "test_object_123", + "camera": "front_door", + "current_zones": ["driveway", "front_yard"], + } + + # Build classification data using helper + classification_data = self._build_classification_data( + obj_data, "sub_label", "person_walking" + ) + + requestor.send_data("tracked_object_update", json.dumps(classification_data)) + + # Verify that send_data was called + requestor.send_data.assert_called_once() + + # Get the actual call arguments + call_args = requestor.send_data.call_args + topic = call_args[0][0] + data_json = call_args[0][1] + + # Verify the topic + self.assertEqual(topic, "tracked_object_update") + + # Parse and verify the data + data = json.loads(data_json) + self.assertEqual(data["type"], "classification") + self.assertEqual(data["id"], "test_object_123") + self.assertEqual(data["camera"], "front_door") + self.assertEqual(data["model"], "test_classifier") + self.assertEqual(data["sub_label"], "person_walking") + self.assertIn("zones", data) + self.assertEqual(data["zones"], ["driveway", "front_yard"]) + + def test_sub_label_message_excludes_zones_when_empty(self): + """Test that zones are not included when object is not in any zones""" + requestor = MagicMock() + + # Create mock obj_data without zones + obj_data = { + "id": "test_object_456", + "camera": "back_door", + "current_zones": [], + } + + # Build classification data using helper + classification_data = self._build_classification_data( + obj_data, "sub_label", "person_running" + ) + classification_data["score"] = 0.87 + + requestor.send_data("tracked_object_update", json.dumps(classification_data)) + + # Get the actual call arguments + call_args = requestor.send_data.call_args + data_json = call_args[0][1] + + # Parse and verify the data + data = json.loads(data_json) + self.assertNotIn("zones", data) + + def test_attribute_message_includes_zones_when_present(self): + """Test that zones are included in attribute classification messages when object is in zones""" + requestor = MagicMock() + + # Create mock obj_data with zones + obj_data = { + "id": "test_object_789", + "camera": "construction_site", + "current_zones": ["site_entrance"], + } + + # Build classification data using helper + classification_data = self._build_classification_data( + obj_data, "attribute", "wearing_helmet" + ) + classification_data["score"] = 0.92 + classification_data["model"] = "helmet_detector" + + requestor.send_data("tracked_object_update", json.dumps(classification_data)) + + # Get the actual call arguments + call_args = requestor.send_data.call_args + data_json = call_args[0][1] + + # Parse and verify the data + data = json.loads(data_json) + self.assertEqual(data["type"], "classification") + self.assertEqual(data["id"], "test_object_789") + self.assertEqual(data["camera"], "construction_site") + self.assertEqual(data["model"], "helmet_detector") + self.assertEqual(data["attribute"], "wearing_helmet") + self.assertIn("zones", data) + self.assertEqual(data["zones"], ["site_entrance"]) + + def test_attribute_message_excludes_zones_when_missing(self): + """Test that zones are not included when current_zones key is missing""" + requestor = MagicMock() + + # Create mock obj_data without current_zones key + obj_data = { + "id": "test_object_999", + "camera": "parking_lot", + } + + # Build classification data using helper + classification_data = self._build_classification_data( + obj_data, "attribute", "sedan" + ) + classification_data["score"] = 0.95 + classification_data["model"] = "vehicle_type" + + requestor.send_data("tracked_object_update", json.dumps(classification_data)) + + # Get the actual call arguments + call_args = requestor.send_data.call_args + data_json = call_args[0][1] + + # Parse and verify the data + data = json.loads(data_json) + self.assertNotIn("zones", data) + + +class TestCustomObjectClassificationIntegration(unittest.TestCase): + """ + Integration tests that call process_frame() on the actual processor. + These tests exercise the full pipeline from process_frame() through the + deferred worker thread to drain_results(), verifying that zone information + is carried through to the result dicts that the maintainer publishes. + """ + + def setUp(self): + import numpy as np + + self.np = np + + # Import the module first so patch() can resolve its attributes. + try: + import frigate.data_processing.real_time.custom_classification # noqa: F401 + from frigate.data_processing.real_time.custom_classification import ( + CustomObjectClassificationProcessor, + ) + except ImportError as e: + self.skipTest(f"Requires full Frigate environment: {e}") + return + + self.ProcessorClass = CustomObjectClassificationProcessor + + # Patch out heavy I/O helpers on the already-imported module object. + for target in [ + "frigate.data_processing.real_time.custom_classification.write_classification_attempt", + "frigate.data_processing.real_time.custom_classification.suppress_stderr_during", + ]: + patcher = patch(target) + patcher.start() + self.addCleanup(patcher.stop) + + def _make_processor(self, classification_type): + """Return a processor with a live interpreter stub and pre-loaded history.""" + config = MagicMock() + model_config = MagicMock() + model_config.name = "test_model" + model_config.threshold = 0.7 + model_config.save_attempts = 100 + model_config.object_config.objects = ["person"] + model_config.object_config.classification_type = classification_type + + sub_label_publisher = MagicMock() + requestor = MagicMock() + metrics = MagicMock() + metrics.classification_speeds = {} + metrics.classification_cps = {} + + with patch.object( + self.ProcessorClass, + "_CustomObjectClassificationProcessor__build_detector", + ): + processor = self.ProcessorClass( + config, model_config, sub_label_publisher, requestor, metrics + ) + + return processor + + def _run_and_drain(self, processor, obj_data, label, score=0.92): + """ + Run process_frame() with a stubbed interpreter and return drain_results(). + Pre-loads 3 identical history entries so consensus is reached immediately. + """ + import numpy as np + + # Pre-load history so get_weighted_score returns consensus on the first call. + processor.classification_history[obj_data["id"]] = [ + (label, score, 1234567890.0), + (label, score, 1234567891.0), + (label, score, 1234567892.0), + ] + + processor.tensor_input_details = [{"index": 0}] + processor.tensor_output_details = [{"index": 0}] + processor.labelmap = {0: label} + + mock_interp = MagicMock() + mock_interp.get_tensor.return_value = np.array([[score, 1.0 - score]]) + processor.interpreter = mock_interp + + frame = np.zeros((WIDTH * 3 // 2, HEIGHT), dtype=np.uint8) + processor.process_frame(obj_data, frame) + + # Give the worker thread time to process the enqueued task. + time.sleep(0.2) + + return processor.drain_results() + + def test_process_frame_with_zones_includes_zones_in_mqtt(self): + """process_frame() with non-empty current_zones must emit a result with zones.""" + from frigate.config.classification import ObjectClassificationType + + processor = self._make_processor(ObjectClassificationType.sub_label) + + obj_data = { + "id": "test_123", + "camera": "front_door", + "label": "person", + "false_positive": False, + "end_time": None, + "box": [100, 100, 200, 200], + "current_zones": ["driveway", "porch"], + } + + results = self._run_and_drain(processor, obj_data, "walking") + + self.assertTrue(results, "process_frame must produce at least one result") + result = results[0] + self.assertEqual(result["type"], "classification") + self.assertIn( + "zones", result, "Result must include zones when object is in zones" + ) + self.assertEqual(result["zones"], ["driveway", "porch"]) + self.assertEqual(result["label"], "walking") + + def test_process_frame_without_zones_excludes_zones_from_mqtt(self): + """process_frame() with empty current_zones must emit a result without zones.""" + from frigate.config.classification import ObjectClassificationType + + processor = self._make_processor(ObjectClassificationType.sub_label) + + obj_data = { + "id": "test_456", + "camera": "backyard", + "label": "person", + "false_positive": False, + "end_time": None, + "box": [150, 150, 250, 250], + "current_zones": [], + } + + results = self._run_and_drain(processor, obj_data, "running") + + self.assertTrue(results, "process_frame must produce at least one result") + result = results[0] + self.assertNotIn("zones", result, "Empty zones should be excluded from result") + + def test_process_frame_attribute_type_includes_zones(self): + """process_frame() with attribute classification type must include zones.""" + from frigate.config.classification import ObjectClassificationType + + processor = self._make_processor(ObjectClassificationType.attribute) + + obj_data = { + "id": "test_789", + "camera": "garage", + "label": "person", + "false_positive": False, + "end_time": None, + "box": [200, 200, 300, 300], + "current_zones": ["parking_lot"], + } + + results = self._run_and_drain(processor, obj_data, "hat") + + self.assertTrue(results, "process_frame must produce at least one result") + result = results[0] + self.assertIn("zones", result, "Result must include zones for attribute type") + self.assertEqual(result["zones"], ["parking_lot"]) + self.assertEqual(result["label"], "hat") + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/track/tracked_object.py b/frigate/track/tracked_object.py index 03117df6924..d7d3dd0417b 100644 --- a/frigate/track/tracked_object.py +++ b/frigate/track/tracked_object.py @@ -35,7 +35,7 @@ # (ex: car loitering on the street vs when a known person parks on the street) # person is the main object that should keep alerts going as long as they loiter # even if they are stationary. -EXTENDED_LOITERING_OBJECTS = ["person"] +EXTENDED_LOITERING_OBJECTS = ["person", "waste_bin", "package"] class TrackedObject: diff --git a/web/package-lock.json b/web/package-lock.json index cd09b79811e..ec1ff7764d5 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -98,14 +98,14 @@ "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/strftime": "^0.9.8", - "@typescript-eslint/eslint-plugin": "^7.5.0", - "@typescript-eslint/parser": "^7.5.0", + "@typescript-eslint/eslint-plugin": "^8.65.0", + "@typescript-eslint/parser": "^8.65.0", "@vitejs/plugin-react-swc": "^3.8.0", "@vitest/coverage-v8": "^3.0.7", "autoprefixer": "^10.4.20", - "eslint": "^8.57.0", + "eslint": "^10.8.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-jest": "^28.2.0", + "eslint-plugin-jest": "^29.16.0", "eslint-plugin-prettier": "^5.0.1", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.8", @@ -704,61 +704,97 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", "dev": true, + "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" }, - "funding": { - "url": "https://opencollective.com/eslint" + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@floating-ui/core": { @@ -808,19 +844,42 @@ "react-hook-form": "^7.0.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" + "@humanfs/types": "^0.15.0" }, "engines": { - "node": ">=10.10.0" + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" } }, "node_modules/@humanwhocodes/module-importer": { @@ -836,12 +895,19 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, - "license": "BSD-3-Clause" + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@inquirer/ansi": { "version": "1.0.2", @@ -5693,6 +5759,13 @@ "@types/ms": "*" } }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -5816,145 +5889,159 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.12.0.tgz", - "integrity": "sha512-7F91fcbuDf/d3S8o21+r3ZncGIke/+eWk0EpO21LXhDfLahriZF9CGj4fbAetEjlaBdjdSm9a6VeXbpbT6Z40Q==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", + "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.12.0", - "@typescript-eslint/type-utils": "7.12.0", - "@typescript-eslint/utils": "7.12.0", - "@typescript-eslint/visitor-keys": "7.12.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/type-utils": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.5.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "@typescript-eslint/parser": "^8.65.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.12.0.tgz", - "integrity": "sha512-lib96tyRtMhLxwauDWUp/uW3FMhLA6D0rJ8T7HmH7x23Gk1Gwwu8UZ94NMXBvOELn6flSPiBrCKlehkiXyaqwA==", + "node_modules/@typescript-eslint/parser": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz", + "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "7.12.0", - "@typescript-eslint/utils": "7.12.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "debug": "^4.4.3" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.12.0.tgz", - "integrity": "sha512-Y6hhwxwDx41HNpjuYswYp6gDbkiZ8Hin9Bf5aJQn1bpTs3afYY4GX+MPYxma8jtoIV2GRwTM/UJm/2uGCVv+DQ==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", + "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.12.0", - "@typescript-eslint/types": "7.12.0", - "@typescript-eslint/typescript-estree": "7.12.0" + "@typescript-eslint/tsconfig-utils": "^8.65.0", + "@typescript-eslint/types": "^8.65.0", + "debug": "^4.4.3" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/parser": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.12.0.tgz", - "integrity": "sha512-dm/J2UDY3oV3TKius2OUZIFHsomQmpHtsV0FTh1WO8EKgHLQ1QCADUqscPgTpU+ih1e21FQSRjXckHn3txn6kQ==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", + "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "7.12.0", - "@typescript-eslint/types": "7.12.0", - "@typescript-eslint/typescript-estree": "7.12.0", - "@typescript-eslint/visitor-keys": "7.12.0", - "debug": "^4.3.4" + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", + "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependencies": { - "eslint": "^8.56.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.12.0.tgz", - "integrity": "sha512-itF1pTnN6F3unPak+kutH9raIkL3lhH1YRPGgt7QQOh43DQKVJXmWkpb+vpc/TiDHs6RSd9CTbDsc/Y+Ygq7kg==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", + "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.12.0", - "@typescript-eslint/visitor-keys": "7.12.0" + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.12.0.tgz", - "integrity": "sha512-o+0Te6eWp2ppKY3mLCU+YA9pVJxhUJE15FV7kxuD9jgwIAa+w/ycGJBMrYDTpVGUM/tgpa9SeMOugSabWFq7bg==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", + "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -5962,78 +6049,88 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.12.0.tgz", - "integrity": "sha512-5bwqLsWBULv1h6pn7cMW5dXX/Y2amRqLaKqsASVwbBHMZSnHqE/HN4vT4fE0aFsiwxYvr98kqOWh1a8ZKXalCQ==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", + "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.12.0", - "@typescript-eslint/visitor-keys": "7.12.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" + "@typescript-eslint/project-service": "8.65.0", + "@typescript-eslint/tsconfig-utils": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "node_modules/@typescript-eslint/utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", + "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.12.0.tgz", - "integrity": "sha512-uZk7DevrQLL3vSnfFl5bj4sL75qC9D6EdjemIdbtkuUmIheWpuiiylSY01JxJE7+zGrOWDZrp1WxOuDntvKrHQ==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", + "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.12.0", - "eslint-visitor-keys": "^3.4.3" + "@typescript-eslint/types": "8.65.0", + "eslint-visitor-keys": "^5.0.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -6194,10 +6291,11 @@ "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==" }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -6242,9 +6340,9 @@ } }, "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -6385,16 +6483,6 @@ "dequal": "^2.0.3" } }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -6481,7 +6569,8 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "node_modules/big-integer": { "version": "1.6.52", @@ -6513,12 +6602,26 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/brace-expansion/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/braces": { @@ -6639,16 +6742,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -6989,11 +7082,6 @@ "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz", "integrity": "sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==" }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -7110,9 +7198,9 @@ } }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -7324,37 +7412,11 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/dom-accessibility-api": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", @@ -7523,59 +7585,62 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz", + "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", "dev": true, "license": "MIT", + "workspaces": [ + "packages/*" + ], "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^10.2.5", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-config-prettier": { @@ -7591,21 +7656,22 @@ } }, "node_modules/eslint-plugin-jest": { - "version": "28.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.6.0.tgz", - "integrity": "sha512-YG28E1/MIKwnz+e2H7VwYPzHUYU4aMa19w0yGcwXnnmJH6EfgHahTJ2un3IyraUxNfnz/KUhJAFXNNwWPo12tg==", + "version": "29.16.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-29.16.0.tgz", + "integrity": "sha512-0WFBxDHlT2ratGQfnFQEVIsgQJ5cfd+0IV8Kc6U3X2onB8ATLG23voD2Ch5G9fCkEpCPmCMuzW0tbS0kYb8biw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/utils": "^6.0.0 || ^7.0.0" + "@typescript-eslint/utils": "^8.0.0" }, "engines": { - "node": "^16.10.0 || ^18.12.0 || >=20.0.0" + "node": "^20.12.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^6.0.0 || ^7.0.0", - "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", - "jest": "*" + "@typescript-eslint/eslint-plugin": "^8.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "jest": "*", + "typescript": ">=4.8.4 <8.0.0" }, "peerDependenciesMeta": { "@typescript-eslint/eslint-plugin": { @@ -7613,32 +7679,12 @@ }, "jest": { "optional": true + }, + "typescript": { + "optional": true } } }, - "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/utils": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.12.0.tgz", - "integrity": "sha512-Y6hhwxwDx41HNpjuYswYp6gDbkiZ8Hin9Bf5aJQn1bpTs3afYY4GX+MPYxma8jtoIV2GRwTM/UJm/2uGCVv+DQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.12.0", - "@typescript-eslint/types": "7.12.0", - "@typescript-eslint/typescript-estree": "7.12.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - } - }, "node_modules/eslint-plugin-prettier": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", @@ -7699,17 +7745,19 @@ "dev": true }, "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -7720,6 +7768,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -7727,29 +7776,56 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^5.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -7775,6 +7851,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -8015,16 +8092,16 @@ } }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/file-selector": { @@ -8077,24 +8154,23 @@ } }, "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "dev": true, "license": "ISC" }, @@ -8216,11 +8292,6 @@ "node": ">= 10.0.0" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -8340,26 +8411,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -8371,43 +8422,6 @@ "node": ">=10.13.0" } }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -8427,12 +8441,6 @@ "dev": true, "license": "ISC" }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, "node_modules/graphql": { "version": "16.8.1", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz", @@ -8705,29 +8713,6 @@ "node": ">=18" } }, - "node_modules/i18next-cli/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/i18next-cli/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, "node_modules/i18next-cli/node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -8828,22 +8813,6 @@ "node": "20 || >=22" } }, - "node_modules/i18next-cli/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/i18next-cli/node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -8939,23 +8908,6 @@ "url": "https://opencollective.com/immer" } }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -8974,20 +8926,6 @@ "node": ">=8" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, "node_modules/inline-style-parser": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", @@ -9176,15 +9114,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -9647,12 +9576,6 @@ "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "license": "MIT" }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, "node_modules/log-symbols": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", @@ -10722,15 +10645,19 @@ } }, "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "license": "ISC", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.8" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -11116,14 +11043,6 @@ "node": ">=0.8.0" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/onetime": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", @@ -11301,19 +11220,6 @@ "dev": true, "license": "BlueOak-1.0.0" }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/parse-entities": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", @@ -11451,14 +11357,6 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -11497,16 +11395,6 @@ "dev": true, "license": "MIT" }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -12512,16 +12400,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -12564,23 +12442,6 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rollup": { "version": "4.60.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", @@ -12829,9 +12690,9 @@ } }, "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { @@ -12899,16 +12760,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/sonner": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", @@ -13092,19 +12943,6 @@ "node": ">=8" } }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/style-to-js": { "version": "1.1.21", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", @@ -13124,16 +12962,17 @@ } }, "node_modules/sucrase": { - "version": "3.34.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", - "integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==", + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", - "glob": "7.1.6", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { @@ -13141,26 +12980,7 @@ "sucrase-node": "bin/sucrase-node" }, "engines": { - "node": ">=8" - } - }, - "node_modules/sucrase/node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=16 || 14 >=14.17" } }, "node_modules/supports-color": { @@ -13384,24 +13204,24 @@ } }, "node_modules/test-exclude": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", - "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", "dev": true, "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^10.4.1", - "minimatch": "^9.0.4" + "minimatch": "^10.2.2" }, "engines": { "node": ">=18" } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -13445,12 +13265,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -13488,7 +13302,6 @@ "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -13505,7 +13318,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -13523,7 +13335,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -13649,16 +13460,16 @@ } }, "node_modules/ts-api-utils": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", - "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, "node_modules/ts-interface-checker": { @@ -13684,19 +13495,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -14472,11 +14270,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, "node_modules/ws": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", diff --git a/web/package.json b/web/package.json index 7d22dc7183a..d52baaa13a6 100644 --- a/web/package.json +++ b/web/package.json @@ -112,14 +112,14 @@ "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/strftime": "^0.9.8", - "@typescript-eslint/eslint-plugin": "^7.5.0", - "@typescript-eslint/parser": "^7.5.0", + "@typescript-eslint/eslint-plugin": "^8.65.0", + "@typescript-eslint/parser": "^8.65.0", "@vitejs/plugin-react-swc": "^3.8.0", "@vitest/coverage-v8": "^3.0.7", "autoprefixer": "^10.4.20", - "eslint": "^8.57.0", + "eslint": "^10.8.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-jest": "^28.2.0", + "eslint-plugin-jest": "^29.16.0", "eslint-plugin-prettier": "^5.0.1", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.8", diff --git a/web/public/locales/en/views/classificationModel.json b/web/public/locales/en/views/classificationModel.json index 3206ad0339a..f0504c3ed8c 100644 --- a/web/public/locales/en/views/classificationModel.json +++ b/web/public/locales/en/views/classificationModel.json @@ -13,7 +13,12 @@ "trainModel": "Train Model", "addClassification": "Add Classification", "deleteModels": "Delete Models", - "editModel": "Edit Model" + "editModel": "Edit Model", + "categorizeImages": "Classify Images", + "enableSelection": "Enable Selection", + "disableSelection": "Disable Selection", + "selectImage": "Select Image", + "selectGroup": "Select Group" }, "tooltip": { "trainingInProgress": "Model is currently training", @@ -26,6 +31,7 @@ "deletedModel_one": "Successfully deleted {{count}} model", "deletedModel_other": "Successfully deleted {{count}} models", "categorizedImage": "Successfully Classified Image", + "batchCategorized": "Successfully classified {{count}} images", "reclassifiedImage": "Successfully Reclassified Image", "trainedModel": "Successfully trained model.", "trainingModel": "Successfully started model training.", @@ -34,18 +40,26 @@ "deletedCategory_one": "Deleted {{count}} class", "deletedCategory_other": "Deleted {{count}} classes", "deletedImage_one": "Deleted {{count}} image", - "deletedImage_other": "Deleted {{count}} images" + "deletedImage_other": "Deleted {{count}} images", + "batchCategorized_one": "Successfully classified {{count}} image", + "batchCategorized_other": "Successfully classified {{count}} images" }, "error": { "deleteImageFailed": "Failed to delete: {{errorMessage}}", "deleteCategoryFailed": "Failed to delete class: {{errorMessage}}", "deleteModelFailed": "Failed to delete model: {{errorMessage}}", "categorizeFailed": "Failed to categorize image: {{errorMessage}}", + "batchCategorizeFailed": "Failed to classify {{count}} images", "trainingFailed": "Model training failed. Check Frigate logs for details.", "trainingFailedToStart": "Failed to start model training: {{errorMessage}}", "updateModelFailed": "Failed to update model: {{errorMessage}}", "renameCategoryFailed": "Failed to rename class: {{errorMessage}}", - "reclassifyFailed": "Failed to reclassify image: {{errorMessage}}" + "reclassifyFailed": "Failed to reclassify image: {{errorMessage}}", + "batchCategorizeFailed_one": "Failed to classify {{count}} image", + "batchCategorizeFailed_other": "Failed to classify {{count}} images" + }, + "warning": { + "partialBatchCategorized": "Classified {{success}} of {{total}} images successfully." } }, "deleteCategory": { diff --git a/web/public/locales/en/views/explore.json b/web/public/locales/en/views/explore.json index d1087b3c96a..d1fc857794b 100644 --- a/web/public/locales/en/views/explore.json +++ b/web/public/locales/en/views/explore.json @@ -146,6 +146,15 @@ }, "recognizedLicensePlate": "Recognized License Plate", "attributes": "Classification Attributes", + "assignment": { + "title": "Assign To", + "assignToFace": "Assign to Face", + "assignToClassification": "Assign to {{model}}", + "faceSuccess": "Successfully assigned to face: {{name}}", + "faceFailed": "Failed to assign to face: {{errorMessage}}", + "classificationSuccess": "Successfully assigned to {{model}} - {{category}}", + "classificationFailed": "Failed to assign classification: {{errorMessage}}" + }, "estimatedSpeed": "Estimated Speed", "objects": "Objects", "camera": "Camera", @@ -222,7 +231,7 @@ "label": "Hide object path" }, "debugReplay": { - "label": "Debug Replay", + "label": "Debug replay", "aria": "View this tracked object in the debug replay view" }, "more": { diff --git a/web/public/locales/en/views/faceLibrary.json b/web/public/locales/en/views/faceLibrary.json index 27e54546016..d385dc2bfc3 100644 --- a/web/public/locales/en/views/faceLibrary.json +++ b/web/public/locales/en/views/faceLibrary.json @@ -32,11 +32,7 @@ "title": "Recent Recognitions", "titleShort": "Recent", "aria": "Select recent recognitions", - "empty": "There are no recent face recognition attempts", - "emptyNoLibrary": { - "title": "Upload a face", - "description": "You must add at least one face to the library for face recognition to function." - } + "empty": "There are no recent face recognition attempts" }, "deleteFaceLibrary": { "title": "Delete Name", @@ -57,7 +53,12 @@ "renameFace": "Rename Face", "deleteFace": "Delete Face", "uploadImage": "Upload Image", - "reprocessFace": "Reprocess Face" + "reprocessFace": "Reprocess Face", + "trainFaces": "Train Faces", + "enableSelection": "Enable Selection", + "disableSelection": "Disable Selection", + "selectImage": "Select Image", + "selectGroup": "Select Group" }, "imageEntry": { "validation": { @@ -83,8 +84,11 @@ "deletedName_other": "{{count}} faces have been successfully deleted.", "renamedFace": "Successfully renamed face to {{name}}", "trainedFace": "Successfully trained face.", + "batchTrainedFaces": "Successfully trained {{count}} faces.", "reclassifiedFace": "Successfully reclassified face.", - "updatedFaceScore": "Successfully updated face score to {{name}} ({{score}})." + "updatedFaceScore": "Successfully updated face score to {{name}} ({{score}}).", + "batchTrainedFaces_one": "Successfully trained {{count}} face.", + "batchTrainedFaces_other": "Successfully trained {{count}} faces." }, "error": { "uploadingImageFailed": "Failed to upload image: {{errorMessage}}", @@ -93,8 +97,14 @@ "deleteNameFailed": "Failed to delete name: {{errorMessage}}", "renameFaceFailed": "Failed to rename face: {{errorMessage}}", "trainFailed": "Failed to train: {{errorMessage}}", + "batchTrainFailed": "Failed to train {{count}} faces.", "reclassifyFailed": "Failed to reclassify face: {{errorMessage}}", - "updateFaceScoreFailed": "Failed to update face score: {{errorMessage}}" + "updateFaceScoreFailed": "Failed to update face score: {{errorMessage}}", + "batchTrainFailed_one": "Failed to train {{count}} face.", + "batchTrainFailed_other": "Failed to train {{count}} faces." + }, + "warning": { + "partialBatchTrained": "Trained {{success}} of {{total}} faces successfully." } } } diff --git a/web/src/components/card/ClassificationCard.tsx b/web/src/components/card/ClassificationCard.tsx index 4fed7e23857..7b4660eb5c3 100644 --- a/web/src/components/card/ClassificationCard.tsx +++ b/web/src/components/card/ClassificationCard.tsx @@ -44,6 +44,7 @@ type ClassificationCardProps = { i18nLibrary: string; showArea?: boolean; count?: number; + topLeftContent?: React.ReactNode; onClick: (data: ClassificationItemData, meta: boolean) => void; children?: React.ReactNode; }; @@ -61,6 +62,7 @@ export const ClassificationCard = forwardRef< i18nLibrary, showArea = true, count, + topLeftContent, onClick, children, }, @@ -143,6 +145,15 @@ export const ClassificationCard = forwardRef< onLoad={() => setImageLoaded(true)} src={`${baseUrl}${data.filepath}`} /> + {topLeftContent && ( +
e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + > + {topLeftContent} +
+ )} {count && (
@@ -199,6 +210,7 @@ type GroupedClassificationCardProps = { i18nLibrary: string; objectType: string; noClassificationLabel?: string; + topLeftContent?: React.ReactNode; onClick: (data: ClassificationItemData | undefined) => void; children?: (data: ClassificationItemData) => React.ReactNode; }; @@ -209,6 +221,7 @@ export function GroupedClassificationCard({ selectedItems, i18nLibrary, noClassificationLabel = "details.none", + topLeftContent, onClick, children, }: GroupedClassificationCardProps) { @@ -314,6 +327,7 @@ export function GroupedClassificationCard({ clickable={true} i18nLibrary={i18nLibrary} count={group.length} + topLeftContent={topLeftContent} onClick={(_, meta) => { if (meta || selectedItems.length > 0) { onClick(undefined); diff --git a/web/src/components/overlay/ClassificationSelectionDialog.tsx b/web/src/components/overlay/ClassificationSelectionDialog.tsx index 470c75ec7fa..7a74966efb0 100644 --- a/web/src/components/overlay/ClassificationSelectionDialog.tsx +++ b/web/src/components/overlay/ClassificationSelectionDialog.tsx @@ -33,9 +33,10 @@ type ClassificationSelectionDialogProps = { className?: string; classes: string[]; modelName: string; - image: string; + image?: string; + images?: string[]; onRefresh?: () => void; - onCategorize?: (category: string) => void; + onCategorize?: (category: string, images?: string[]) => void; excludeCategory?: string; dialogLabel?: string; tooltipLabel?: string; @@ -46,6 +47,7 @@ export default function ClassificationSelectionDialog({ classes, modelName, image, + images, onRefresh, onCategorize, excludeCategory, @@ -57,35 +59,105 @@ export default function ClassificationSelectionDialog({ const onCategorizeImage = useCallback( (category: string) => { + const targetImages = + images?.length && image && !images.includes(image) + ? [image, ...images] + : images?.length + ? images + : image + ? [image] + : []; + + // If custom categorize handler is provided, use it instead if (onCategorize) { - onCategorize(category); + onCategorize(category, targetImages); + return; + } + + if (targetImages.length === 0) { + toast.error(t("toast.error.batchCategorizeFailed", { count: 0 }), { + position: "top-center", + }); return; } - axios - .post(`/classification/${modelName}/dataset/categorize`, { - category, - training_file: image, - }) - .then((resp) => { - if (resp.status == 200) { - toast.success(t("toast.success.categorizedImage"), { + if (targetImages.length === 1) { + // Default behavior: categorize a single image. + axios + .post(`/classification/${modelName}/dataset/categorize`, { + category, + training_file: targetImages[0], + }) + .then((resp) => { + if (resp.status == 200) { + toast.success(t("toast.success.categorizedImage"), { + position: "top-center", + }); + onRefresh?.(); + } + }) + .catch((error) => { + const errorMessage = + error.response?.data?.message || + error.response?.data?.detail || + "Unknown error"; + toast.error(t("toast.error.categorizeFailed", { errorMessage }), { position: "top-center", }); - onRefresh?.(); - } - }) - .catch((error) => { - const errorMessage = - error.response?.data?.message || - error.response?.data?.detail || - "Unknown error"; - toast.error(t("toast.error.categorizeFailed", { errorMessage }), { - position: "top-center", }); - }); + return; + } + + const requests = targetImages.map((filename) => + axios + .post(`/classification/${modelName}/dataset/categorize`, { + category, + training_file: filename, + }) + .then(() => true) + .catch(() => false), + ); + + Promise.allSettled(requests).then((results) => { + const successCount = results.filter( + (result) => result.status === "fulfilled" && result.value, + ).length; + const totalCount = results.length; + + if (successCount === totalCount) { + toast.success( + t("toast.success.batchCategorized", { + count: successCount, + }), + { + position: "top-center", + }, + ); + } else if (successCount > 0) { + toast.warning( + t("toast.warning.partialBatchCategorized", { + success: successCount, + total: totalCount, + }), + { + position: "top-center", + }, + ); + } else { + toast.error( + t("toast.error.batchCategorizeFailed", { + count: totalCount, + }), + { + position: "top-center", + }, + ); + } + + onRefresh?.(); + }); }, - [modelName, image, onRefresh, onCategorize, t], + [modelName, image, images, onRefresh, onCategorize, t], ); const filteredClasses = useMemo( @@ -102,19 +174,6 @@ export default function ClassificationSelectionDialog({ // control const [newClass, setNewClass] = useState(false); - // Non-modal Radix DropdownMenu doesn't propagate wheel events to nested - // scroll containers, so attach a non-passive listener that scrolls manually. - const scrollContainerRef = useCallback((el: HTMLDivElement | null) => { - if (!el || !isDesktop) return; - const handleWheel = (e: WheelEvent) => { - if (el.scrollHeight <= el.clientHeight) return; - e.preventDefault(); - el.scrollTop += e.deltaY; - }; - el.addEventListener("wheel", handleWheel, { passive: false }); - return () => el.removeEventListener("wheel", handleWheel); - }, []); - // components const Selector = isDesktop ? DropdownMenu : Drawer; const SelectorTrigger = isDesktop ? DropdownMenuTrigger : DrawerTrigger; @@ -127,70 +186,68 @@ export default function ClassificationSelectionDialog({ ); - // keep modal false on desktop to prevent dismissable layer pointer events - // issue with dialog auto-close return ( -
+
onCategorizeImage(newCat)} /> - - - - {children} - - - {tooltipLabel ?? t("categorizeImage")} - - - e.preventDefault()} - > - {isMobile && ( - - Details - Details - - )} - - {dialogLabel ?? t("categorizeImageAs")} - -
- {filteredClasses - .sort((a, b) => { - if (a === "none") return 1; - if (b === "none") return -1; - return a.localeCompare(b); - }) - .map((category) => ( - onCategorizeImage(category)} - > - {category === "none" - ? t("details.none") - : category.replaceAll("_", " ")} - - ))} - - setNewClass(true)} + + + + + {children} + + e.preventDefault()} + > + {isMobile && ( + + Details + Details + + )} + + {dialogLabel ?? t("categorizeImageAs")} + +
- {t("createCategory.new")} - -
-
-
+ {filteredClasses + .sort((a, b) => { + if (a === "none") return 1; + if (b === "none") return -1; + return a.localeCompare(b); + }) + .map((category) => ( + onCategorizeImage(category)} + > + {category === "none" + ? t("details.none") + : category.replaceAll("_", " ")} + + ))} + + setNewClass(true)} + > + {t("createCategory.new")} + +
+
+
+ {tooltipLabel ?? t("categorizeImage")} +
); } diff --git a/web/src/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx index e8e4368ea42..3ff008f7658 100644 --- a/web/src/components/overlay/detail/SearchDetailDialog.tsx +++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx @@ -94,6 +94,11 @@ import { useDetailStream } from "@/context/detail-stream-context"; import { PiSlidersHorizontalBold } from "react-icons/pi"; import { HiSparkles } from "react-icons/hi"; import { useAudioTranscriptionProcessState } from "@/api/ws"; +import FaceSelectionDialog from "@/components/overlay/FaceSelectionDialog"; +import ClassificationSelectionDialog from "@/components/overlay/ClassificationSelectionDialog"; +import { FaceLibraryData } from "@/types/face"; +import AddFaceIcon from "@/components/icons/AddFaceIcon"; +import { TbCategoryPlus } from "react-icons/tb"; const SEARCH_TABS = ["snapshot", "tracking_details"] as const; export type SearchTab = (typeof SEARCH_TABS)[number]; @@ -241,7 +246,7 @@ function AnnotationSettings({ return (
- +
+ {isAdmin && (availableFaceNames.length > 0 || availableClassificationModels.length > 0) && ( +
+
+ {t("details.assignment.title")} +
+
+ {config?.face_recognition?.enabled && availableFaceNames.length > 0 && ( + + + + )} + {availableClassificationModels.length > 0 && + availableClassificationModels.map((modelName) => { + const model = config?.classification?.custom?.[modelName]; + if (!model) return null; + + const displayName = model.name || modelName; + const classes = modelAttributes?.[displayName] ?? []; + if (classes.length === 0) return null; + + return ( + {}} + onCategorize={(category) => + onAssignToClassification(modelName, category) + } + > + + + ); + })} +
+
+ )} + {isAdmin && search.data.type === "object" && config?.plus?.enabled && @@ -1581,7 +1721,7 @@ function ObjectDetailsTab({ {t("button.yes", { ns: "common" })}