diff --git a/README.md b/README.md index 55f7f5b..b1b49fd 100644 --- a/README.md +++ b/README.md @@ -120,9 +120,9 @@ If you need to test SIO analytics service and don't have an available RTSP sourc Next, copy your test video file to the live555 mount path: ```bash -mkdir -p "${SH_BASE}"/services/live555/test-data +mkdir -p "${SH_BASE}"/media/input/video/live555 # cp or scp -cp "${SH_BASE}"/services/live555/test-data/my-video.mp4 +cp "${SH_BASE}"/media/input/video/live555/my-video.mp4 ``` You can also execute this by running: diff --git a/RELEASE.md b/RELEASE.md index 3637479..20db8a5 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,5 +1,14 @@ # Release Notes +## v1.5.3 +- Add Aqueduct API and UI examples +- Refine Aqueduct runner example +- Add fakeRTSP example configuration +- Update live555 +- Update MCP to 1.3.2 +- Update SIO to r231120 +- Fix samples' compatibility with DNNCam/DNNNode +- Allow samples to define SIO image version via SIO_RELEASE define ## v1.5.2 diff --git a/VERSION b/VERSION index a503124..f1a2e63 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v1.5.2 +v1.5.3 diff --git a/configurations/fakeRTSP.conf b/configurations/fakeRTSP.conf new file mode 100644 index 0000000..fb54113 --- /dev/null +++ b/configurations/fakeRTSP.conf @@ -0,0 +1,13 @@ +select_live555_video +disable amqp-stats +remove_orphans +enable live555 +enable mcp +enable rabbitmq +enable sio +select_example sio live555 +up live555 +up rabbitmq +up mcp +test_rtsp_stream rtsp://localhost:7554/data/my-video.mkv 5 +restart sio \ No newline at end of file diff --git a/deployment-examples/SighthoundRestApiGateway/docker-compose.yml b/deployment-examples/SighthoundRestApiGateway/docker-compose.yml index cf44c2a..50dc31e 100644 --- a/deployment-examples/SighthoundRestApiGateway/docker-compose.yml +++ b/deployment-examples/SighthoundRestApiGateway/docker-compose.yml @@ -2,7 +2,7 @@ version: "3" services: analytics: - image: us-central1-docker.pkg.dev/ext-edge-analytics/docker/sio:r230828 + image: us-central1-docker.pkg.dev/ext-edge-analytics/docker/sio:${SIO_RELEASE-r231120}${SIO_DOCKER_TAG_VARIANT} restart: unless-stopped environment: # Location where SIO will place generated model engine files diff --git a/deployment-examples/StandaloneSIOWithExtension/README.md b/deployment-examples/StandaloneSIOWithExtension/README.md index abf9017..e0db91f 100644 --- a/deployment-examples/StandaloneSIOWithExtension/README.md +++ b/deployment-examples/StandaloneSIOWithExtension/README.md @@ -1,4 +1,4 @@ -# Standalone Folder Watcher Deployment of SIO +# Standalone SIO sample In this example, SIO is deployed in a stand-alone manner, without a RabbitMQ broker or client processing the results. Instead, a pipeline extension is used to process and report the pipeline output. @@ -6,10 +6,12 @@ Instead, a pipeline extension is used to process and report the pipeline output. Contact [support@sighthound.com](mailto:support@sighthound.com) with any questions, and visit our [Developer Portal](https://dev.sighthound.com) for more information. -## Running the sample +## General Before getting started, you must copy your `sighthound-license.json` into the `./StandaloneSIOWithExtension/config/` folder. If you do not have a license, please contact [support@sighthound.com](mailto:support@sighthound.com). +## Running the folder watcher sample + Ensure `./StandaloneSIOWithExtension/data/input` exists prior to starting the service. Next, open a terminal, `cd` into the `./StandaloneSIOWithExtension/` folder, and run the following command to start the services: @@ -26,3 +28,18 @@ SIO_DOCKER_RUNTIME=nvidia docker compose up -d You can then deposit images and videos into `./StandaloneSIOWithExtension/data/input` and watch the output being printed as those are being procesed. +## Running the RTSP sample + +RTSP sample will consume the video feed via specified URL. The sample is packaged with a video file streamed from `live555` service container. +You can remove that service from `docker-compose-rtsp` if pointing the configuration in `config/analytics/pipelines-rtsp.json` to a different RTSP URL. + +You start the sample in a manner similar to folder watcher sample, using `docker compose -f docker-compose-rtsp.yml up -d` command to start the service. + +This configuration also demonstrates how to adapt the pipeline to a particular camera view. In this case, we see cars driving on the street, +cars making the right turn from the right lane and cars making the left turn from the left lane. +For the cars on the street we almost never see their license plates; thus it makes most sense to exclude those entirely. This is done by setting `lptSkipCarsWithoutLPs` +to `true`. The cars in the left lane may be seen or not, depending on whether there are cars in the right lane to occlude them. We expect that no one would want a +camera deployed in such non-deterministic way - thus assuming that monitoring the right turn lane is the purpose of this camera. Setting filters in `boxFilter-rtsp.json` +ensures we're not going to analyze objects outside of the bottom right quadrant ROI (`lp_roiFilter` accomplishes that), and won't analyze objects below size threshold (`vehicle_SizeFilter` +and `lp_SizeFilter` filters accomplish that). + diff --git a/deployment-examples/StandaloneSIOWithExtension/config/analytics/boxFilter-rtsp.json b/deployment-examples/StandaloneSIOWithExtension/config/analytics/boxFilter-rtsp.json new file mode 100644 index 0000000..605ac69 --- /dev/null +++ b/deployment-examples/StandaloneSIOWithExtension/config/analytics/boxFilter-rtsp.json @@ -0,0 +1,28 @@ +[ + { + "name": "lp_roiFilter", + "type" : "roi", + "region" : [ 0.5, 0.5, 0.99999, 0.99999 ], + "behavior": "boxInRoi", + "classes": ["licenseplate"], + "debug": false + }, + { + "name": "lp_SizeFilter", + "type": "size", + "subtype": "dimension", + "max": 0, + "min": 30, + "classes": ["licenseplate", "motorbike"], + "debug": false + }, + { + "name": "vehicle_SizeFilter", + "type": "size", + "subtype": "dimension", + "max": 0, + "min": 40, + "classes": ["car", "bus", "truck"], + "debug": false + } +] diff --git a/deployment-examples/StandaloneSIOWithExtension/config/analytics/pipelines-rtsp.json b/deployment-examples/StandaloneSIOWithExtension/config/analytics/pipelines-rtsp.json new file mode 100644 index 0000000..019cc25 --- /dev/null +++ b/deployment-examples/StandaloneSIOWithExtension/config/analytics/pipelines-rtsp.json @@ -0,0 +1,26 @@ +{ + "unifiedEU-US" : { + "pipeline" : "./share/pipelines/VehicleAnalytics/VehicleAnalyticsRTSP.yaml", + "restartPolicy" : "restart", + "parameters" : { + "VIDEO_IN" : "rtsp://live555_svc:554/Turn-02.mkv", + "boxFilterConfig" : "/config/analytics/boxFilter-rtsp.json", + "detectionModel" : "gen6es", + "lptFilter" : "['us']", + "mmcFilter" : "['us']", + "lptMinConfidence" : "0.5", + "sourceId" : "fw-1", + "lptPreferAccuracyToSpeed" : "true", + "extensionModules" : "/config/analytics/extension.py", + "extensionConfigurations" : "/config/analytics/extensionConfig1.json", + "useTracker" : "true", + "updateOnlyOnChange" : "true", + "lptStabilizationDelay" : "5", + "updateOnImprovedScore" : "true", + "debugSettings" : "log,json", + "lptSkipCarsWithoutLPs" : "true" + }, + "disabledParameters" : { + } + } +} diff --git a/deployment-examples/StandaloneSIOWithExtension/docker-compose-rtsp.yml b/deployment-examples/StandaloneSIOWithExtension/docker-compose-rtsp.yml new file mode 100644 index 0000000..3e5a514 --- /dev/null +++ b/deployment-examples/StandaloneSIOWithExtension/docker-compose-rtsp.yml @@ -0,0 +1,43 @@ +version: "2.3" +services: + + # By default pipelines.json will point to streams served by this container. + # If you point to your own cameras or streams, this container has no other + # function and can be disabled. + live555_svc: + image: us-central1-docker.pkg.dev/ext-edge-analytics/docker/live555:2.0.4-examples + container_name: sample-live555 + restart: unless-stopped + ports: + - "8554:554" + + + analytics: + image: us-central1-docker.pkg.dev/ext-edge-analytics/docker/sio:${SIO_RELEASE-r231120}${SIO_DOCKER_TAG_VARIANT} + restart: unless-stopped + environment: + # Location where SIO will place generated model engine files + - SIO_DATA_DIR=/data/sio-cache + # We need this to see output from Python extension module + - PYTHONUNBUFFERED=1 + # Container runtime defaults to `runc` if SIO_DOCKER_RUNTIME not set. Use `nvidia` if GPU is installed. + runtime: ${SIO_DOCKER_RUNTIME-runc} + volumes: + # Read-only shared folder for data exchange with host / other containers. + # We'll use it for license, config files, etc. + - ./config:/config:ro + # Writable shared folder for data exchange with host + # We'll use it for storing the generated model files, data exchange folder, etc. + - ./data:/data + entrypoint: + - /sighthound/sio/bin/runPipelineSet + # Pipeline configuration file + - /config/analytics/pipelines-rtsp.json + # License at the path accessible in the container + - --license-path=/config/sighthound-license.json + # Log level (info, debug, trace) + - --log=${SIO_LOG_LEVEL-info} + depends_on: + # This dependency can be removed with live555 if no longer necessary. + - live555_svc + diff --git a/deployment-examples/StandaloneSIOWithExtension/docker-compose.yml b/deployment-examples/StandaloneSIOWithExtension/docker-compose.yml index 4d74703..3b6a438 100644 --- a/deployment-examples/StandaloneSIOWithExtension/docker-compose.yml +++ b/deployment-examples/StandaloneSIOWithExtension/docker-compose.yml @@ -1,8 +1,8 @@ -version: "3" +version: "2.3" services: analytics: - image: us-central1-docker.pkg.dev/ext-edge-analytics/docker/sio:r231024 + image: us-central1-docker.pkg.dev/ext-edge-analytics/docker/sio:${SIO_RELEASE-r231120}${SIO_DOCKER_TAG_VARIANT} restart: unless-stopped environment: # Location where SIO will place generated model engine files diff --git a/deployment-examples/VideoStreamsConsumer/README.md b/deployment-examples/VideoStreamsConsumer/README.md index de9e78f..12dc230 100644 --- a/deployment-examples/VideoStreamsConsumer/README.md +++ b/deployment-examples/VideoStreamsConsumer/README.md @@ -28,6 +28,12 @@ If you have an NVIDIA GPU installed and properly configured, you can run the fol SIO_DOCKER_RUNTIME=nvidia docker compose up -d ``` +Or if you have an NVIDIA Tegra device properly configured (e/g DNNcam), you can run the following command instead to enable GPU acceleration: + +```bash +SIO_DOCKER_RUNTIME=nvidia SIO_DOCKER_TAG_VARIANT="-r32.7.3-arm64v8" docker-compose up -d +``` + ## Running the client sample Once the services are up and running, start the client sample with a `docker compose up` command while inside the relevant sample's folder in `./clients/python/`. diff --git a/deployment-examples/VideoStreamsConsumer/docker-compose.yml b/deployment-examples/VideoStreamsConsumer/docker-compose.yml index 8bcf18b..e123944 100644 --- a/deployment-examples/VideoStreamsConsumer/docker-compose.yml +++ b/deployment-examples/VideoStreamsConsumer/docker-compose.yml @@ -1,11 +1,11 @@ -version: "3" +version: "2.3" services: # By default pipelines.json will point to streams served by this container. # If you point to your own cameras or streams, this container has no other # function and can be disabled. live555_svc: - image: us-central1-docker.pkg.dev/ext-edge-analytics/docker/live555:latest + image: us-central1-docker.pkg.dev/ext-edge-analytics/docker/live555:2.0.4-examples container_name: sample-live555 restart: unless-stopped ports: @@ -31,7 +31,7 @@ services: # The SIO analytics container, consuming the streams and analyzing them analytics_svc: - image: us-central1-docker.pkg.dev/ext-edge-analytics/docker/sio:r230908 + image: us-central1-docker.pkg.dev/ext-edge-analytics/docker/sio:${SIO_RELEASE-r231120}${SIO_DOCKER_TAG_VARIANT} container_name: sample-sio restart: unless-stopped environment: diff --git a/deployment-examples/VideoStreamsRecorder/docker-compose.yml b/deployment-examples/VideoStreamsRecorder/docker-compose.yml index 93b17a8..e53af16 100644 --- a/deployment-examples/VideoStreamsRecorder/docker-compose.yml +++ b/deployment-examples/VideoStreamsRecorder/docker-compose.yml @@ -5,7 +5,7 @@ services: # If you point to your own cameras or streams, this container has no other # function and can be disabled. live555_svc: - image: us-central1-docker.pkg.dev/ext-edge-analytics/docker/live555:2.0.3-examples + image: us-central1-docker.pkg.dev/ext-edge-analytics/docker/live555:2.0.4-examples container_name: sample-live555 restart: unless-stopped ports: @@ -52,7 +52,7 @@ services: # The SIO analytics container, consuming the streams and analyzing them analytics_svc: - image: us-central1-docker.pkg.dev/ext-edge-analytics/docker/sio:r230908${SIO_DOCKER_TAG_VARIANT} + image: us-central1-docker.pkg.dev/ext-edge-analytics/docker/sio:${SIO_RELEASE-r231120}${SIO_DOCKER_TAG_VARIANT} container_name: sample-sio restart: unless-stopped environment: diff --git a/docs/schemas/anypipe/anypipe.html b/docs/schemas/anypipe/anypipe.html index 6c3d79d..4397ee2 100644 --- a/docs/schemas/anypipe/anypipe.html +++ b/docs/schemas/anypipe/anypipe.html @@ -1 +1 @@ - Sighthound Analytics

Sighthound Analytics

Type: object

Analytics data sent by the Sighthound video/image analysis pipeline. This data is sent based on configuration when the number of detected objects or attributes of detected objects changes, the confidence of detected objects or their attributes improves, or a configurable timeout occurs.

No Additional Properties

Type: object

Type: integer

Timestamp the frame corresponding to this analytics data was processed at, in milliseconds since the epoch and GMT timezone.

Value must be greater or equal to 0

Type: string

A global unique ID representing the media source, for
instance a specific video stream from a camera sensor or RTSP feed, , or input source location for images or videos

Type: string

An ID corresponding to this frame, which may be used to
access the image corresponding to all box coordinates and object
detections represented in this object, via the Media Service API.

Type: object

The dimensions (width and height) of the frame represented by frameId. Also used as the coordinate base for all bounding box coordinates.

Type: number

Width in pixels

Value must be greater or equal to 0

Type: number

Height in pixels

Value must be greater or equal to 0

Type: integer

Timestamp of the frame corresponding to this analytics data, acccording to the source, in milliseconds since the epoch and GMT timezone.

Value must be greater or equal to 0

Type: string

Type: object

Meta classes include objects such as vehicles, license plates, and people. These are high-level classifications.

All properties whose name matches the following regular expression must respect the following conditions

Property name regular expression: ^.*$
Type: object

An plural MetaClass name. Supported MetaClasses
include:
vehicles - Objects including cars, buses, trucks, motorbikes.
Vehicles include objects which may potentially include license
plates, may include links to licensePlates.
licensePlates - Objects which are detected/classified as license plates.
people - Pedestrians or people riding skateboards, electric
scooter, wheelchairs,etc.

All properties whose name matches the following regular expression must respect the following conditions

Property name regular expression: ^.*$
Type: object

A Unique ID representing this object, used to map
additional object properties. This ID is guaranteed unique
for each object, regardless of streamId. It will change the object drops out of
detection/tracking

Type: integer

The analyticsTimestamp with highest confidence score for this object.

Value must be greater or equal to 0

Type: string

Object specific class returned by the model. For objects of the vehicles metaclass this may include car, truck, bus, motorbike, etc based on model capabilities

Type: object

All properties whose name matches the following regular expression must respect the following conditions

Property name regular expression: ^.*$
Type: object

A map of attributes for this object. Not all atributes are supported for all object types. Example attributes include:
color - The color of an object
lpString - A string representing license plate text
and numbers
lpRegion - A string representing license plate region
vehicleType - Make model and generation of the vehicle in a single string

No Additional Properties

Type: number

Confidence score for attribute detection, ranging from 0.0 to 1.0. A score of 1.0 indicates 100% confidence.

Value must be greater or equal to 0 and lesser or equal to 1

Type: number

Confidence score for object detection, ranging from 0.0 to 1.0. A score of 1.0 indicates 100% confidence.When included in an attribute, this score represents the
object Detection score for the parent object corresponding to the
timestamp when the attribute value was determined.

Value must be greater or equal to 0 and lesser or equal to 1

Type: boolean

Flag to indicate if the attribute is updated. True means updated, False means not updated.


A value of the attribute. The value is specific to the attribute type.

Type: object

Information about the detected vehicle, including its make, model, and generation.

Type: string

The manufacturer of the detected vehicle, e.g., 'Toyota'.

Type: string

The specific model of the detected vehicle, e.g., 'Camry'.

Type: string

The generation or variant of the detected vehicle, e.g., '2020'.

Type: string

The category to which the detected vehicle belongs, e.g., 'Sedan'.

Additional Properties of any type are allowed.

Type: object

Type: object

Debug information, subject to change
between releases. Do not use this object in an
application.

All properties whose name matches the following regular expression must respect the following conditions

Property name regular expression: ^.*$
Type: string

Type: string

An object hash which uniquely identifies this object and associated attributes. Will change when attributes change. Reserved for future use

Type: object

The bounding box containing this object, in
pixel coordinates where the top left corner of the
image is represented by pixel 0,0, corresponding to the image referenced by imageRef

No Additional Properties

Type: integer

Height of the bounding box in pixels

Value must be greater or equal to 0

Type: integer

Width of the bounding box in pixels

Value must be greater or equal to 0

Type: integer

X coordinate of the top left corner
of the bounding box.

Value must be greater or equal to 0

Type: integer

Y coordinate of the top left corner of
the bounding box

Value must be greater or equal to 0

Type: number

Confidence score for object detection, ranging from 0.0 to 1.0. A score of 1.0 indicates 100% confidence.When included in an attribute, this score represents the
object Detection score for the parent object corresponding to the
timestamp when the attribute value was determined.

Same definition as detectionScore

Type: boolean

Flag to indicate if the attribute is updated. True means updated, False means not updated.

Same definition as updated

Type: integer

The analyticsTimestamp with highest confidence score for this object.

Value must be greater or equal to 0

Type: object

A map of maps describing an event type.
- The top level map key is a name describing the event type. Supported types are presenceSensor, lineCrossingEvent, speedEvent.
- The sub level map key is a Unique ID representing the event, used to map
additional object properties. This ID is guaranteed unique
for each event for a given stream ID.

All properties whose name matches the following regular expression must respect the following conditions

Property name regular expression: ^.*$
Type: object

A name describing an event type.

All properties whose name matches the following regular expression must respect the following conditions

Property name regular expression: ^.*$
Type: array

A Unique ID representing this event

No Additional Items

Each item of this array must be:


Type: object

Describes an event where one or more objects are present in a region of interest.
The event starts when the first object enters a region of interest. Updates are sent for each change in status, with updateCount incremented for each update. When the last object exits and the region is empty, the sensor event will become immutable and will track the total amount of time at least one object was present in the region of interest. An entry of an object will start a new event and reset the updateCount to 1. Region definitons, object filtering and other items related to sensor definitions are tracked as a part of the sensorId associated with the event.

No Additional Properties

Type: string

The globally unique event ID corresponding to this event.

Type: integer

The total number of objects of a specific type detected within a region of interest, excluding those filtered out based on sensor configuration.

Value must be greater or equal to 0

Type: object

The total number of detected objects in a region grouped by metaclasses.

All properties whose name matches the following regular expression must respect the following conditions

Property name regular expression: ^.*$
Type: integer

The total number of objects detected within a region of interest grouped by metaclass. Metaclasses represent higher-level categories that objects may belong to, such as 'vehicle' or 'people,' while classes represent more specific types, such as 'car' or 'person'.

Value must be greater or equal to 0

Type: object

The total number of detected objects in a region grouped by classes.

All properties whose name matches the following regular expression must respect the following conditions

Property name regular expression: ^.*$
Type: integer

The total number of objects detected within a region of interest grouped by class. For example, if the sensor is configured to detect vehicles, this property may include counts of 'car,' 'bus,' and 'truck'.

Value must be greater or equal to 0

Type: integer

The time in milliseconds since the epoch (GMT) when the event started, or when a link was established.

Value must be greater or equal to 0

Type: integer

The cumulative number of updates sent for this sensor, starting with 1 for the initial update and incremented once for each update sent for each unique sensor event ID. An update refers to a change in the state of the sensor due to a corresponding sensor event (entry, exit, crossing, ...). For sensors which include multiple updates per sensor event (presense sensors), the updateCount will be reset to 1 to indicate the first update for a given event. For sensors (count) which only include 1 update per event, updateCount will be cumulative and count the total number of events per sensor.

Value must be greater or equal to 0

Type: integer

The time in milliseconds since the epoch (GMT) when the event ended.

Value must be greater or equal to 0

Type: object

Describes an event where one object crosses a line

No Additional Properties

Type: string

The globally unique event ID corresponding to this event.

Same definition as eventId

Type: string

The direction of an object's trajectory relative to the sensor's line, with the first point (A) as the pivot point. 'Clockwise' means the object is moving in a clockwise direction relative to the line, while 'counterclockwise' means the object is moving in a counterclockwise direction.

Type: integer

Number of clockwise crossings.

Value must be greater or equal to 0

Type: integer

Number of counterclockwise crossings.

Value must be greater or equal to 0

Type: integer

The time in milliseconds since the epoch (GMT) when the event started, or when a link was established.

Same definition as startedAt

Type: array of object
No Additional Items

Each item of this array must be:

Type: object

Type: string

Media Event type: Ex: image,video

Type: string

Message content

Type: integer

Start of Event Timestamp

Value must be greater or equal to 0

Type: integer

End of Event Timestamp

Value must be greater or equal to 0

Type: string

Message format. Ex: json, jpeg, mp4, ts...

\ No newline at end of file + Sighthound Analytics

Sighthound Analytics

Type: object

Analytics data sent by the Sighthound video/image analysis pipeline. This data is sent based on configuration when the number of detected objects or attributes of detected objects changes, the confidence of detected objects or their attributes improves, or a configurable timeout occurs.

No Additional Properties

Type: object

Type: integer

Timestamp the frame corresponding to this analytics data was processed at, in milliseconds since the epoch and GMT timezone.

Value must be greater or equal to 0

Type: string

A global unique ID representing the media source, for
instance a specific video stream from a camera sensor or RTSP feed, , or input source location for images or videos

Type: string

An ID corresponding to this frame, which may be used to
access the image corresponding to all box coordinates and object
detections represented in this object, via the Media Service API.

Type: object

The dimensions (width and height) of the frame represented by frameId. Also used as the coordinate base for all bounding box coordinates.

Type: number

Width in pixels

Value must be greater or equal to 0

Type: number

Height in pixels

Value must be greater or equal to 0

Type: integer

Timestamp of the frame corresponding to this analytics data, acccording to the source, in milliseconds since the epoch and GMT timezone.

Value must be greater or equal to 0

Type: string

Type: object

Meta classes include objects such as vehicles, license plates, and people. These are high-level classifications.

All properties whose name matches the following regular expression must respect the following conditions

Property name regular expression: ^.*$
Type: object

An plural MetaClass name. Supported MetaClasses
include:
vehicles - Objects including cars, buses, trucks, motorbikes.
Vehicles include objects which may potentially include license
plates, may include links to licensePlates.
licensePlates - Objects which are detected/classified as license plates.
people - Pedestrians or people riding skateboards, electric
scooter, wheelchairs,etc.

All properties whose name matches the following regular expression must respect the following conditions

Property name regular expression: ^.*$
Type: object

A Unique ID representing this object, used to map
additional object properties. This ID is guaranteed unique
for each object, regardless of streamId. It will change the object drops out of
detection/tracking

Type: integer

The analyticsTimestamp with highest confidence score for this object.

Value must be greater or equal to 0

Type: string

Object specific class returned by the model. For objects of the vehicles metaclass this may include car, truck, bus, motorbike, etc based on model capabilities

Type: object

All properties whose name matches the following regular expression must respect the following conditions

Property name regular expression: ^.*$
Type: object

A map of attributes for this object. Not all atributes are supported for all object types. Example attributes include:
color - The color of an object
lpString - A string representing license plate text
and numbers
lpRegion - A string representing license plate region
vehicleType - Make model and generation of the vehicle in a single string

No Additional Properties

Type: number

Confidence score for attribute detection, ranging from 0.0 to 1.0. A score of 1.0 indicates 100% confidence.

Value must be greater or equal to 0 and lesser or equal to 1

Type: number

Confidence score for object detection, ranging from 0.0 to 1.0. A score of 1.0 indicates 100% confidence.When included in an attribute, this score represents the
object Detection score for the parent object corresponding to the
timestamp when the attribute value was determined.

Value must be greater or equal to 0 and lesser or equal to 1

Type: boolean

Flag to indicate if the attribute is updated. True means updated, False means not updated.


A value of the attribute. The value is specific to the attribute type.

Type: object

Information about the detected vehicle, including its make, model, and generation.

Type: string

The manufacturer of the detected vehicle, e.g., 'Toyota'.

Type: string

The specific model of the detected vehicle, e.g., 'Camry'.

Type: string

The generation or variant of the detected vehicle, e.g., '2020'.

Type: string

The category to which the detected vehicle belongs, e.g., 'Sedan'.

Additional Properties of any type are allowed.

Type: object

Type: object

Debug information, subject to change
between releases. Do not use this object in an
application.

All properties whose name matches the following regular expression must respect the following conditions

Property name regular expression: ^.*$
Type: string

Type: string

An object hash which uniquely identifies this object and associated attributes. Will change when attributes change. Reserved for future use

Type: object

The bounding box containing this object, in
pixel coordinates where the top left corner of the
image is represented by pixel 0,0, corresponding to the image referenced by imageRef

No Additional Properties

Type: integer

Height of the bounding box in pixels

Value must be greater or equal to 0

Type: integer

Width of the bounding box in pixels

Value must be greater or equal to 0

Type: integer

X coordinate of the top left corner
of the bounding box.

Value must be greater or equal to 0

Type: integer

Y coordinate of the top left corner of
the bounding box

Value must be greater or equal to 0

Type: number

Confidence score for object detection, ranging from 0.0 to 1.0. A score of 1.0 indicates 100% confidence.When included in an attribute, this score represents the
object Detection score for the parent object corresponding to the
timestamp when the attribute value was determined.

Same definition as detectionScore

Type: boolean

Flag to indicate if the attribute is updated. True means updated, False means not updated.

Same definition as updated

Type: integer

The analyticsTimestamp with highest confidence score for this object.

Value must be greater or equal to 0

Type: object

A map of maps describing an event type.
- The top level map key is a name describing the event type. Supported types are presenceSensor, lineCrossingEvent, speedEvent.
- The sub level map key is a Unique ID representing the event, used to map
additional object properties. This ID is guaranteed unique
for each event for a given stream ID.

All properties whose name matches the following regular expression must respect the following conditions

Property name regular expression: ^.*$
Type: object

A name describing an event type.

All properties whose name matches the following regular expression must respect the following conditions

Property name regular expression: ^.*$
Type: array

A Unique ID representing this event

No Additional Items

Each item of this array must be:


Type: object

Describes an event where one or more objects are present in a region of interest.
The event starts when the first object enters a region of interest. Updates are sent for each change in status, with updateCount incremented for each update. When the last object exits and the region is empty, the sensor event will become immutable and will track the total amount of time at least one object was present in the region of interest. An entry of an object will start a new event and reset the updateCount to 1. Region definitons, object filtering and other items related to sensor definitions are tracked as a part of the sensorId associated with the event.

No Additional Properties

Type: string

The globally unique event ID corresponding to this event.

Type: integer

The total number of objects of a specific type detected within a region of interest, excluding those filtered out based on sensor configuration.

Value must be greater or equal to 0

Type: object

The total number of detected objects in a region grouped by metaclasses.

All properties whose name matches the following regular expression must respect the following conditions

Property name regular expression: ^.*$
Type: integer

The total number of objects detected within a region of interest grouped by metaclass. Metaclasses represent higher-level categories that objects may belong to, such as 'vehicle' or 'people,' while classes represent more specific types, such as 'car' or 'person'.

Value must be greater or equal to 0

Type: object

The total number of detected objects in a region grouped by classes.

All properties whose name matches the following regular expression must respect the following conditions

Property name regular expression: ^.*$
Type: integer

The total number of objects detected within a region of interest grouped by class. For example, if the sensor is configured to detect vehicles, this property may include counts of 'car,' 'bus,' and 'truck'.

Value must be greater or equal to 0

Type: integer

The time in milliseconds since the epoch (GMT) when the event started, or when a link was established.

Value must be greater or equal to 0

Type: integer

The cumulative number of updates sent for this sensor, starting with 1 for the initial update and incremented once for each update sent for each unique sensor event ID. An update refers to a change in the state of the sensor due to a corresponding sensor event (entry, exit, crossing, ...). For sensors which include multiple updates per sensor event (presense sensors), the updateCount will be reset to 1 to indicate the first update for a given event. For sensors (count) which only include 1 update per event, updateCount will be cumulative and count the total number of events per sensor.

Value must be greater or equal to 0

Type: integer

The time in milliseconds since the epoch (GMT) when the event ended.

Value must be greater or equal to 0

Type: object

Describes an event where one object crosses a line

No Additional Properties

Type: string

The globally unique event ID corresponding to this event.

Same definition as eventId

Type: string

The direction of an object's trajectory relative to the sensor's line, with the first point (A) as the pivot point. 'Clockwise' means the object is moving in a clockwise direction relative to the line, while 'counterclockwise' means the object is moving in a counterclockwise direction.

Type: integer

Number of clockwise crossings.

Value must be greater or equal to 0

Type: integer

Number of counterclockwise crossings.

Value must be greater or equal to 0

Type: integer

The time in milliseconds since the epoch (GMT) when the event started, or when a link was established.

Same definition as startedAt

Type: array of object
No Additional Items

Each item of this array must be:

Type: object

Type: string

Media Event type: Ex: image,video

Type: string

Message content

Type: integer

Start of Event Timestamp

Value must be greater or equal to 0

Type: integer

End of Event Timestamp

Value must be greater or equal to 0

Type: string

Message format. Ex: json, jpeg, mp4, ts...

\ No newline at end of file diff --git a/examples/AqueductAPI/Dockerfile b/examples/AqueductAPI/Dockerfile new file mode 100644 index 0000000..acf935d --- /dev/null +++ b/examples/AqueductAPI/Dockerfile @@ -0,0 +1,22 @@ +# Use an official Python runtime as a parent image +FROM python:3.8-slim-buster + +# Working directory +WORKDIR /app + +# Install dependencies +COPY requirements.txt /app/ +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the application into the container +# COPY ../lib/ /app/lib +COPY ./src/ /app/ + +# Make port 5000 available +EXPOSE 5000 +ENV PYTHONPATH=${PYTHONPATH}:/app/:/app/lib/ +# TERM environment variable not set. +ENV TERM=xterm-256color + +# Run app.py when the container launches +CMD ["python", "app.py"] diff --git a/examples/AqueductAPI/README.md b/examples/AqueductAPI/README.md new file mode 100644 index 0000000..70ca0ac --- /dev/null +++ b/examples/AqueductAPI/README.md @@ -0,0 +1,32 @@ +# Aqueduct API + +This is a Flask-based API to manage video analytics pipelines. The API provides functionalities to start, stop, delete, and fetch the status of pipelines. + +## Features +- Start Pipeline: Initialize a new pipeline with specified parameters. +- Stop Pipeline: Terminate an existing pipeline. +- Delete Pipeline: Remove an existing pipeline. +- Fetch Status: Retrieve the status of all pipelines or a single pipeline. + +## How to run +First start all the dependencies: +```bash +./scripts sh-services apply ./examples/AqueductAPI/aqueduct.conf +cd ./examples/AqueductAPI +``` + +And then build and run the Docker Compose: +```bash +docker compose build +docker compose up +``` + +Your API should now be accessible at http://localhost:8888. + +## API Endpoints +- POST `/pipelines/start`: Starts a new pipeline. +- POST `/pipelines/stop`: Stops an existing pipeline. +- POST `/pipelines/delete`: Deletes an existing pipeline. +- GET `/pipelines/status`: Gets the status of all pipelines. +- GET `/pipelines/status/`: Gets the status of a specific pipeline. +- GET `/health`: Health check. diff --git a/examples/AqueductAPI/aqueduct.conf b/examples/AqueductAPI/aqueduct.conf new file mode 100644 index 0000000..a199836 --- /dev/null +++ b/examples/AqueductAPI/aqueduct.conf @@ -0,0 +1,13 @@ +select_live555_video +disable amqp-stats +remove_orphans +enable live555 +enable mcp +enable rabbitmq +enable sio +select_example sio aqueduct +up live555 +up rabbitmq +up mcp +test_rtsp_stream rtsp://localhost:7554/data/my-video.mkv 5 +restart sio \ No newline at end of file diff --git a/examples/AqueductAPI/docker-compose.yml b/examples/AqueductAPI/docker-compose.yml new file mode 100644 index 0000000..6e6990d --- /dev/null +++ b/examples/AqueductAPI/docker-compose.yml @@ -0,0 +1,20 @@ +version: "2" + +services: + aqueduct-api: + build: . + restart: unless-stopped + volumes: + - ../lib:/app/lib + - ../../..:/data/sighthound + - ./src:/app + ports: + - "8888:8888" + networks: + core_sighthound: + aliases: + - aqueduct-api + +networks: + core_sighthound: + external: true \ No newline at end of file diff --git a/examples/AqueductAPI/requirements.txt b/examples/AqueductAPI/requirements.txt new file mode 100644 index 0000000..0f02354 --- /dev/null +++ b/examples/AqueductAPI/requirements.txt @@ -0,0 +1,9 @@ +Flask==1.1.2 +flask-restx==1.1.0 +flask-cors==3.0.10 +pika==1.2.0 +jinja2==2.11.2 +markupsafe==1.1.1 +itsdangerous==1.1.0 +werkzeug==2.0.3 +tabulate==0.8.9 \ No newline at end of file diff --git a/examples/AqueductAPI/src/app.py b/examples/AqueductAPI/src/app.py new file mode 100644 index 0000000..4b4d83a --- /dev/null +++ b/examples/AqueductAPI/src/app.py @@ -0,0 +1,123 @@ +from flask_restx import Resource, Api, fields, Namespace +from flask import Flask, request +from flask_cors import CORS +import socket +from lib.Aqueduct import AqueductAMQP, Pipelines, subscribe +import os +import logging + +FLASK_PORT = os.getenv('FLASK_PORT', '8888') +AMPQ_HOST = os.getenv('AMPQ_HOST', 'rabbitmq') +AMPQ_PORT = os.getenv('AMPQ_PORT', '5672') +AMPQ_USER = os.getenv('AMPQ_USER', 'guest') +AMPQ_PASSWORD = os.getenv('AMPQ_PASSWORD', 'guest') + +logger = logging.getLogger(__name__) + +app = Flask(__name__) +CORS(app) +api = Api(app, version='1.0', title='Pipeline API', description='API for Managing Pipelines') +ns = api.namespace('pipelines', description='Pipeline operations') + +pipeline_start_model = api.model('Start Pipeline', { + 'sourceId': fields.String(required=True, description='Source ID'), + 'pipeline': fields.String(required=True, description='Pipeline Name'), + 'URL': fields.String(required=True, description='RTSP URL'), + 'extra_parameters': fields.Raw(description='Extra Parameters'), +}) + +pipeline_stop_model = api.model('Stop Pipeline', { + 'sourceId': fields.String(required=True, description='Source ID to stop'), +}) + +# Due to pika restrictions we need one connection per thread +send_publisher = AqueductAMQP("send", AMPQ_HOST, AMPQ_PORT, AMPQ_USER, AMPQ_PASSWORD, logger) +subscribe_publisher = AqueductAMQP("subscribe", AMPQ_HOST, AMPQ_PORT, AMPQ_USER, AMPQ_PASSWORD, logger) + +pipelines_manager = Pipelines(".", "./db.json", send_publisher, logger) + +@ns.route('/start') +class StartPipeline(Resource): + @ns.expect(pipeline_start_model, validate=True) + def post(self): + data = request.json + source_id = data['sourceId'] + if pipelines_manager.exists(source_id): + return {'message': f'Pipeline {source_id} already exists.'}, 400 + if data['pipeline'] not in ["VehicleAnalytics", "TrafficAnalytics"]: + return {'message': f'Pipeline {data["pipeline"]} not supported.'}, 400 + pipelines = { + source_id: { + "pipeline": f"./share/pipelines/{data['pipeline']}/{data['pipeline']}RTSP.yaml", + "parameters": { + "VIDEO_IN": data['URL'], + "sourceId": source_id, + "recordTo": f"/data/sighthound/media/output/video/{source_id}/", + "imageSaveDir": f"/data/sighthound/media/output/image/{source_id}/", + "amqpHost": "rabbitmq", + "amqpPort": "5672", + "amqpExchange": "anypipe", + "amqpUser": "guest", + "amqpPassword": "guest", + "amqpErrorOnFailure": "true", + **data['extra_parameters'] + } + } + } + # Save to simulated DB + pipelines_manager.run(pipelines) + if not pipelines_manager.wait(source_id, ["start", "done"], 20): + return {'message': f'Pipeline {source_id} failed to start. Latest status {pipelines_manager.status(source_id)}.'}, 500 + + return {'message': f'Started pipeline {source_id}. Status: {pipelines_manager.status(source_id)}.'}, 200 + +@ns.route('/stop') +class StopPipeline(Resource): + @ns.expect(pipeline_stop_model, validate=True) + def post(self): + data = request.json + source_id = data['sourceId'] + # Remove from simulated DB + pipelines_manager.stop(source_id) + if not pipelines_manager.wait(source_id, ["stop", "done"], 20): + return {'message': f'Pipeline {source_id} failed to stop. Latest status {pipelines_manager.status(source_id)}.'}, 500 + + return {'message': f'Stopped pipeline {source_id}. Status: {pipelines_manager.status(source_id)}.'}, 200 + +@ns.route('/delete') +class DeletePipeline(Resource): + @ns.expect(pipeline_stop_model, validate=True) + def post(self): + data = request.json + source_id = data['sourceId'] + # Remove from simulated DB + pipelines_manager.delete(source_id) + return {'message': f'Deleted pipeline {source_id}.'}, 200 + +@ns.route('/status') +class Status(Resource): + def get(self): + return pipelines_manager.getDB() + +@ns.route('/status/') +class StatusById(Resource): + def get(self, sourceId): + return pipelines_manager.status(sourceId) + +@api.route('/health') +class Health(Resource): + def get(self): + return {'message': 'Ok'}, 200 + +def onAqueductUpdate(ch, method, properties, body): + pipelines_manager.handle_message(True, method, properties, body) + +def setup(): + socket.gethostbyname(AMPQ_HOST) + send_publisher.connect() + subscribe_publisher.connect() + subscribe(subscribe_publisher, onAqueductUpdate, background=True, exchange="aqueduct", topic='everything') + +if __name__ == '__main__': + setup() + app.run(host='0.0.0.0', port=FLASK_PORT) \ No newline at end of file diff --git a/examples/AqueductRunner/Dockerfile b/examples/AqueductRunner/Dockerfile index ffb4e04..e33735b 100644 --- a/examples/AqueductRunner/Dockerfile +++ b/examples/AqueductRunner/Dockerfile @@ -1,13 +1,12 @@ FROM python:3.8 RUN apt update && apt install -y curl -RUN curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && /usr/bin/python3 get-pip.py WORKDIR /usr/src/app COPY requirements.txt /usr/src/app/ -RUN /usr/bin/python3 -m pip install -r requirements.txt +RUN pip3 install -r requirements.txt -ENV PYTHONPATH=/usr/src/app/:/usr/src/app/lib/ +ENV PYTHONPATH=${PYTHONPATH}:/usr/src/app/:/usr/src/app/lib/ ENV PYTHONUNBUFFERED=1 -ENTRYPOINT [ "/usr/bin/python3", "/usr/src/app/aqueductRunner.py"] +ENTRYPOINT [ "python3", "/usr/src/app/aqueductRunner.py"] diff --git a/examples/AqueductRunner/aqueduct.conf b/examples/AqueductRunner/aqueduct.conf new file mode 100644 index 0000000..a199836 --- /dev/null +++ b/examples/AqueductRunner/aqueduct.conf @@ -0,0 +1,13 @@ +select_live555_video +disable amqp-stats +remove_orphans +enable live555 +enable mcp +enable rabbitmq +enable sio +select_example sio aqueduct +up live555 +up rabbitmq +up mcp +test_rtsp_stream rtsp://localhost:7554/data/my-video.mkv 5 +restart sio \ No newline at end of file diff --git a/examples/AqueductRunner/docker-compose.yml b/examples/AqueductRunner/docker-compose.yml index df696a9..349dbc5 100644 --- a/examples/AqueductRunner/docker-compose.yml +++ b/examples/AqueductRunner/docker-compose.yml @@ -15,6 +15,7 @@ services: - ./src:/usr/src/app - ./pipelines:/usr/src/app/pipelines - ../lib:/usr/src/app/lib + - ../../..:/data/sighthound networks: core_sighthound: aliases: diff --git a/examples/AqueductRunner/pipelines/video_file_input.json b/examples/AqueductRunner/pipelines/video_file_input.json new file mode 100644 index 0000000..d663be7 --- /dev/null +++ b/examples/AqueductRunner/pipelines/video_file_input.json @@ -0,0 +1,17 @@ +{ + "video-file": { + "pipeline": "./share/pipelines/TrafficAnalytics/TrafficAnalyticsRTSP.yaml", + "parameters": { + "VIDEO_IN": "/data/sighthound/media/input/video/my-video.mkv", + "sourceId": "video-file", + "recordTo": "/data/sighthound/media/output/video/video-file/", + "imageSaveDir": "/data/sighthound/media/output/image/video-file/", + "amqpHost": "rabbitmq", + "amqpPort": "5672", + "amqpExchange": "anypipe", + "amqpUser": "guest", + "amqpPassword": "guest", + "amqpErrorOnFailure": "true" + } + } +} \ No newline at end of file diff --git a/examples/AqueductRunner/src/aqueductRunner.py b/examples/AqueductRunner/src/aqueductRunner.py index 7172580..76c52d5 100644 --- a/examples/AqueductRunner/src/aqueductRunner.py +++ b/examples/AqueductRunner/src/aqueductRunner.py @@ -1,16 +1,11 @@ #! /usr/bin/env python3 -import pika import sys -import json import time -import glob import argparse import logging -import os import socket from datetime import datetime -from threading import Thread -from tabulate import tabulate +from lib.Aqueduct import AqueductAMQP, Pipelines, subscribe logger = logging.getLogger("aqueduct") @@ -24,14 +19,11 @@ parser.add_argument('--pipelinesDBFile', help='', default='./db.json') parser.add_argument('--amqpHost', help='AMQP host', default='rabbitmq') parser.add_argument('--amqpPort', help='AMQP port', default=5672, type=int) -parser.add_argument('--amqpUserName','-u', help='AMQP username', default='guest') +parser.add_argument('--amqpUsername','-u', help='AMQP username', default='guest') parser.add_argument('--amqpPassword', '-p', help='AMQP password', default='guest') -parser.add_argument('--amqpExchange', help='AMQP exchange', default='aqueduct') -parser.add_argument('--executeRoutingKey', help='Aqueduct execute routing key', default='aqueduct.execute.default') -parser.add_argument('--controlRoutingKey', help='Aqueduct control key', default='aqueduct.control.default') -parser.add_argument('--statusRoutingKey', help='Aqueduct status key', default='aqueduct.status.#') -parser.add_argument('--everythingRoutingKey', help='Aqueduct status key', default='#') parser.add_argument('--sleep', help='Sleep time between loops', default=10, type=int) +parser.add_argument('--timeout', help='Max timeout for start and stop operations. Zero means infinite', default=0, type=int) +parser.add_argument('--json_dump', help='Dump AMQP messages to json file', default="", type=str) parser.add_argument('command', help='Command to run', choices=['update', 'folderwatch', 'watch', 'run', 'stop', 'ps']) parser.add_argument('args', help='Arguments to pass to the command', nargs='*') @@ -39,328 +31,6 @@ def help(): parser.print_help(sys.stderr) sys.exit(1) -def time_ago(time): - """ - Get a datetime object or a int() Epoch timestamp and return a - pretty string like 'an hour ago', 'Yesterday', '3 months ago', - 'just now', etc - Modified from: http://stackoverflow.com/a/1551394/141084 - """ - - if time == 0: - return 'Never' - now = datetime.now() - if type(time) is int: - diff = now - datetime.fromtimestamp(time) - elif type(time) is float: - diff = now - datetime.fromtimestamp(time) - elif isinstance(time,datetime): - diff = now - time - else: - raise ValueError('invalid date %s of type %s' % (time, type(time))) - second_diff = diff.seconds - day_diff = diff.days - - if day_diff < 0: - return 'error' - - if day_diff == 0: - if second_diff < 10: - return "just now" - if second_diff < 60: - return str(round(second_diff,2)) + " seconds ago" - if second_diff < 120: - return "a minute ago" - if second_diff < 3600: - return str( round(second_diff / 60 ,2)) + " minutes ago" - if second_diff < 7200: - return "an hour ago" - if second_diff < 86400: - return str( round(second_diff / 3600 ,2)) + " hours ago" - if day_diff == 1: - return "Yesterday" - if day_diff < 7: - return str(round(day_diff, 2)) + " days ago" - if day_diff < 31: - return str(round(day_diff/7,2)) + " weeks ago" - if day_diff < 365: - return str(round(day_diff/30, 2)) + " months ago" - return str(round(day_diff/365,2)) + " years ago" - -class Pipelines: - def __init__(self, path, pipelinesDBFile, publisher, _logger): - self.path = path - self.pipelinesDBFile = pipelinesDBFile - self.logger = _logger.getChild('pipelines') - self.publisher = publisher - self.logger.info('Reading pipelines from: %s', self.path) - if self.path != "": - if not os.path.exists(self.path): - os.makedirs(self.path) - if not os.path.isdir(self.path): - self.logger.getChild("init").error('Pipelines path does not exist: %s', self.path) - raise Exception('Pipelines path does not exist: %s', self.path) - else: - self.logger.getChild("init").warning('Pipelines path is empty, will not load any pipelines or save pipelines data') - - # Check if db file exists - if not os.path.isfile(self.pipelinesDBFile): - self.logger.getChild("init").info('Creating db file: %s', self.pipelinesDBFile) - with open(self.pipelinesDBFile, 'w') as f: - f.write('{}') - - def getDB(self): - if self.path == "": - return {} - with open(self.pipelinesDBFile, 'r') as f: - data = json.load(f) - return data - - def writeDB(self, data): - if self.path == "": - return - with open(self.pipelinesDBFile, 'w') as f: - json.dump(data, f, indent=4) - - def handle_message(self, watch_mode, method, props, body): - routing_key = method.routing_key - print(f" [x] Received {routing_key}: {body}") - try: - message = json.loads(body) - if routing_key.startswith("aqueduct.status"): - self.logger.debug(f"Pipeline '{message['sourceId']}' status: {message['cause']}") - self.updateStatusPipeline(message['sourceId'], message['cause']) - if watch_mode: - self.print_pipelines_db(datetime.now().strftime("%H:%M:%S"), self.getDB(), clear=True) - except Exception as e: - print(e) - - - def ps(self): - if self.path == "": - self.logger.getChild("ps").warning('Pipelines path is empty, will not load any pipelines or save pipelines data') - return - self.updateDBFromDisk() - data = self.getDB() - self.print_pipelines_db("Pipelines", data) - - - def updateStatusPipeline(self, id, status): - if self.path == "": - return - pipelines_db = self.getDB() - if id in pipelines_db: - self.logger.info(f"Pipeline {id}, changed status from {pipelines_db[id]['status']} to {status}") - pipelines_db[id]["status"] = status - pipelines_db[id]['lastStatusUpdate'] = int(datetime.now().timestamp()) - else: - self.logger.error(f"Pipeline {id} not found in db") - self.writeDB(pipelines_db) - - def addOrUpdatePipeline(self, id, source, data): - source = os.path.basename(source).replace('.json', '') - if self.path == "": - logger.warn(f"Cannot add or update pipeline {id}") - pipelines_db = self.getDB() - lastUpdate = int(datetime.now().timestamp()) - if id in pipelines_db: - if source != pipelines_db[id]['source']: - self.logger.error(f"Error while updating {id}, source was added by another file {pipelines_db[id]['source']}, but found in {source}") - pipelines_db[id]["data"] = data - pipelines_db[id]["lastUpdate"] = lastUpdate - else: - status = "loaded" - lastStatusUpdate = 0 - created = int(datetime.now().timestamp()) - pipelines_db[id] = {"data": data, "status": status, "lastStatusUpdate": lastStatusUpdate, "lastUpdate": lastUpdate, "created": created, "source": source} - - self.writeDB(pipelines_db) - - def print_pipelines_db(self, title, pipelines_db, clear=False): - if self.path == "": - return [] - if clear: - os.system('cls' if os.name in ('nt', 'dos') else 'clear') - print(title) - table = [['Id', 'Status', 'Last Status Update', 'Last Update', 'Created']] - for id, pipeline in pipelines_db.items(): - created_str = datetime.fromtimestamp(pipeline['created']).strftime('%m/%d/%Y, %H:%M:%S') - lastStatusUpdate_str = time_ago(pipeline['lastStatusUpdate']) - lastUpdate_str = datetime.fromtimestamp(pipeline['lastUpdate']).strftime('%m/%d/%Y, %H:%M:%S') - table.append([id, pipeline['status'], lastStatusUpdate_str, lastUpdate_str, created_str]) - print(tabulate(table)) - - def updateDBFromDisk(self): - for aqueduct_file in glob.glob(self.path + '/*.json'): - try: - with open(aqueduct_file) as f: - data = json.load(f) - for pipeline in data: - self.addOrUpdatePipeline(pipeline, aqueduct_file, data[pipeline]) - except Exception as e: - self.logger.getChild("updateDBFromDisk").error('Error loading pipeline: %s', aqueduct_file) - print(e) - - def get(self, id): - if self.path == "": - return None - for pipeline in glob.glob(self.path + '/*.json'): - try: - with open(pipeline) as f: - data = json.load(f) - if id in data: - return data[id] - except: - self.logger.getChild("get").warning('Failed to load pipeline: %s', pipeline) - return None - - def runFolderWatch(self): - if self.path == "": - self.logger.getChild("runFolder").warning('Pipelines path is empty, will not load any pipelines or save pipelines data') - return - self.updateDBFromDisk() - - for id, data in self.getDB().items(): - logger.info("Running pipeline: %s", id) - self.runPipeline(id, data["data"]) - - def run(self, args): - logger = self.logger.getChild("run") - if len(args) < 1: - logger.error('No pipeline file provided') - logger.info('Example: aqueductRunner run pipeline.json') - raise Exception('No pipeline file provided') - elif len(args) > 1: - logger.error('Too many arguments') - logger.info('Example: aqueductRunner run pipeline.json') - return - print("Running pipelines from : ", args) - source_file = args[0] - with open(source_file) as f: - data = json.load(f) - for id, pipeline in data.items(): - logger.info("Running pipeline: %s", id) - self.addOrUpdatePipeline(id, source_file, pipeline) - self.runPipeline(id, pipeline) - - def runPipeline(self, id, pipeline): - logger.info("Running pipeline: %s", id) - msg = pipeline - msg["command"] = "execute" - msg["sourceId"] = id - self.publisher.publish(json.dumps(msg), 'execute', "." + id) - self.updateStatusPipeline(id, "execute_sent") - - def stopPipeline(self, id): - logger.info("Stopping pipeline: %s", id) - msg = {"command": "stop", "sourceId": id} - self.publisher.publish(json.dumps(msg), 'execute', "." + id) - - def stop(self, args): - logger = self.logger.getChild("stop") - if len(args) < 1: - logger.error('No pipeline file or id provided') - logger.info('Example: aqueductRunner stop pipeline_id/pipeline.json') - return - elif len(args) > 1: - logger.error('Too many arguments') - logger.info('Example: aqueductRunner stop pipeline_id/pipeline.json') - return - try: - pipeline = args[0] - with open(pipeline) as f: - data = json.load(f) - for id, pipeline in data.items(): - logger.info("Stopping pipeline: %s", id) - self.stopPipeline(id) - except: - self.stopPipeline(args[0]) - - - - -class Publisher: - def __init__(self, name, args, logger): - self.logger = logger.getChild("publisher").getChild(name) - self._args = args - self.logger.info(f"Connecting to AMQP on {args.amqpHost}:{args.amqpPort} with user: {args.amqpUserName}...") - self._params = pika.connection.ConnectionParameters( - host=args.amqpHost, - port=args.amqpPort, - virtual_host='/', - credentials=pika.credentials.PlainCredentials(args.amqpUserName, args.amqpPassword)) - self._conn = None - self._channel = None - - - def connect(self): - if not self._conn or self._conn.is_closed: - self.logger.debug('Connecting to AMQP instance: %s:%s', self._params.host, self._params.port) - self._conn = pika.BlockingConnection(self._params) - self._channel = self._conn.channel() - self._channel.exchange_declare(exchange=self._args.amqpExchange, exchange_type='topic', durable=True) - - def _choose_routing_key(self, topic): - if topic == "execute": - return str(self._args.executeRoutingKey) - elif topic == "control": - return str(self._args.controlRoutingKey) - elif topic == "status": - return str(self._args.statusRoutingKey) - elif topic == "everything": - return str(self._args.everythingRoutingKey) - else: - raise Exception("Unknown topic: " + topic) - - def _publish(self, msg, routing_key): - if self._channel is None: - raise Exception('Not connected to queue') - self.logger.info('publishing message: \'%s\' to exchange: "%s" and routing_key: "%s"', msg, self._args.amqpExchange, routing_key) - self._channel.basic_publish(exchange=self._args.amqpExchange, - routing_key=routing_key, - properties=pika.BasicProperties(content_type='application/json'), - body=msg) - self.logger.debug('message sent: %s', msg) - - def publish(self, msg, topic=None, suffix=""): - """Publish msg, reconnecting if necessary.""" - routing_key = self._choose_routing_key(topic) + suffix - try: - self._publish(msg, routing_key) - except pika.exceptions.ConnectionClosed: - self.logger.debug('reconnecting to queue') - self.connect() - self._publish(msg, routing_key) - - def subscribe(self, callback, topic=None): - self.logger.debug('subscribing to "%s":"%s"', self._args.amqpExchange, self._choose_routing_key(topic)) - if self._channel is None: - self.logger.error('channel is not initialized') - return - queue = self._channel.queue_declare(queue='', exclusive=True) - self._channel.queue_bind(exchange=self._args.amqpExchange, - queue=queue.method.queue, - routing_key=self._choose_routing_key(topic)) - self._channel.basic_consume(queue=queue.method.queue, - on_message_callback=callback, - auto_ack=True) - self._channel.start_consuming() - - def close(self): - if self._conn and self._conn.is_open: - self.logger.debug('closing queue connection') - self._conn.close() - -def subscribe(publisher, callback, background=True, topic='status'): - if background: - T = Thread(target = publisher.subscribe, args = (callback, topic)) - # change T to daemon - T.setDaemon(True) - # starting of Thread T - T.start() - else: - publisher.subscribe(callback, topic) - def main(args, logger): logger.debug('Starting aqueduct runner...') @@ -370,11 +40,13 @@ def main(args, logger): logger.error('Failed to connect to AMQP host: %s', args.amqpHost) return # Due to pika restrictions we need one connection per thread - send_publisher = Publisher("send", args, logger) - subscribe_publisher = Publisher("subscribe", args, logger) + send_publisher = AqueductAMQP("send", args.amqpHost, args.amqpPort, args.amqpUsername, args.amqpPassword, logger) + subscribe_publisher = AqueductAMQP("subscribe", args.amqpHost, args.amqpPort, args.amqpUsername, args.amqpPassword, logger) + anypipe_publisher = AqueductAMQP("anypipe", args.amqpHost, args.amqpPort, args.amqpUsername, args.amqpPassword, logger) try: send_publisher.connect() subscribe_publisher.connect() + anypipe_publisher.connect() except Exception as e: logger.error('Failed to connect to AMQP: %s', e) return @@ -383,20 +55,22 @@ def main(args, logger): watch_mode = args.command in ['watch', 'folderwatch'] def onAqueductUpdate(ch, method, properties, body): pipelines.handle_message(watch_mode, method, properties, body) + def onAnypipeMessage(ch, method, properties, body): + pipelines.anypipe_message(method, properties, body, args.json_dump) if args.command == 'update': logger.info('Running update once...') - subscribe(subscribe_publisher, onAqueductUpdate, background=True, topic='everything') + subscribe(subscribe_publisher, onAqueductUpdate, background=True, exchange="aqueduct", topic='everything') pipelines.runFolderWatch() time.sleep(args.sleep) elif args.command == 'watch': logger.info('Running in watch mode...') pipelines.print_pipelines_db(datetime.now().strftime("%H:%M:%S"), pipelines.getDB(), clear=True) - subscribe(subscribe_publisher, onAqueductUpdate, background=False, topic='everything') + subscribe(subscribe_publisher, onAqueductUpdate, background=False, exchange="aqueduct", topic='everything') elif args.command == 'folderwatch': logger.info('Running in folderwatch mode...') - subscribe(subscribe_publisher, onAqueductUpdate, background=True, topic='everything') + subscribe(subscribe_publisher, onAqueductUpdate, background=True, exchange="aqueduct", topic='everything') # Send constant updates while True: pipelines.print_pipelines_db(datetime.now().strftime("%H:%M:%S"), pipelines.getDB(), clear=True) @@ -404,15 +78,33 @@ def onAqueductUpdate(ch, method, properties, body): logger.debug('Sleeping for %s seconds', args.sleep) time.sleep(args.sleep) elif args.command == 'run': - subscribe(subscribe_publisher, onAqueductUpdate, background=True, topic='everything') + if len(args) < 1: + logger.error('No pipeline file provided') + logger.info('Example: aqueductRunner run pipeline.json') + raise Exception('No pipeline file provided') + elif len(args) > 1: + logger.error('Too many arguments') + logger.info('Example: aqueductRunner run pipeline.json') + raise Exception('Too many arguments') + subscribe(subscribe_publisher, onAqueductUpdate, background=True, exchange="aqueduct", topic='everything') + if args.json_dump != "": + subscribe(anypipe_publisher, onAnypipeMessage, background=True, exchange="anypipe", topic='everything') logger.info('Running pipeline') - pipelines.run(args.args) - time.sleep(args.sleep) + pipelines.run_from_file(args[0]) + pipelines.wait(args[0], args.timeout) elif args.command == 'stop': - subscribe(subscribe_publisher, onAqueductUpdate, background=True, topic='everything') + if len(args) < 1: + logger.error('No pipeline file or id provided') + logger.info('Example: aqueductRunner stop pipeline_id/pipeline.json') + return + elif len(args) > 1: + logger.error('Too many arguments') + logger.info('Example: aqueductRunner stop pipeline_id/pipeline.json') + return + subscribe(subscribe_publisher, onAqueductUpdate, background=True, exchange="aqueduct", topic='everything') logger.info('Stopping pipeline') - pipelines.stop(args.args) - time.sleep(args.sleep) + pipelines.stop(args[0]) + pipelines.wait(args[0], args.timeout) elif args.command == 'ps': logger.debug('Listing pipelines') pipelines.ps() diff --git a/examples/AqueductUI/Dockerfile b/examples/AqueductUI/Dockerfile new file mode 100644 index 0000000..aa80361 --- /dev/null +++ b/examples/AqueductUI/Dockerfile @@ -0,0 +1,18 @@ +# Use Node.js image +FROM node:16 + +# Set the working directory +WORKDIR /app + +# Copy package files and install dependencies +COPY src/package*.json ./ +RUN npm install + +# Copy local code to the container +COPY src /app + +# Build the Svelte app +RUN npm run build + +# Start the application +CMD ["npm", "run", "preview"] \ No newline at end of file diff --git a/examples/AqueductUI/README.md b/examples/AqueductUI/README.md new file mode 100644 index 0000000..b3e8840 --- /dev/null +++ b/examples/AqueductUI/README.md @@ -0,0 +1,34 @@ +# Aqueduct UI + +This example depends completely on the other example [Aqueduct UI](../AqueductAPI/) + +## Quickstart + +First of all we need to set SIO in Aqueduct mode. +Do that by running: + +```bash +./scripts/sh-services apply ./examples/AqueductAPI/aqueduct.conf +``` + +This command will ask for a video to use as a fake RTSP, just select one MKV file, and the rest is automatic. + +After the setup, start the Aqueduct API in the background: + +```bash +./scripts/sh-services start_example AqueductAPI +``` + +And check the logs: + +```bash +docker logs aqueductapi-aqueduct-api-1 +``` + +And finally, start the Aqueduct UI: + +```bash +./scripts/sh-services start_example AqueductUI +``` + +And open the website at: http://localhost:4173 \ No newline at end of file diff --git a/examples/AqueductUI/docker-compose.yml b/examples/AqueductUI/docker-compose.yml new file mode 100644 index 0000000..2606d32 --- /dev/null +++ b/examples/AqueductUI/docker-compose.yml @@ -0,0 +1,23 @@ +version: '3.7' +services: + aqueductui: + build: . + container_name: aqueductui + restart: always + ports: + - "4173:4173" + environment: + - MCP_HOST=mcp + - MCP_PORT=9097 + - MCP_USERNAME=root + - MCP_PASSWORD=root + volumes: + - ../../..:/data/sighthound + networks: + core_sighthound: + aliases: + - aqueductui + +networks: + core_sighthound: + external: true \ No newline at end of file diff --git a/examples/AqueductUI/src/.npmrc b/examples/AqueductUI/src/.npmrc new file mode 100644 index 0000000..0c05da4 --- /dev/null +++ b/examples/AqueductUI/src/.npmrc @@ -0,0 +1,2 @@ +engine-strict=true +resolution-mode=highest diff --git a/examples/AqueductUI/src/.svelte-kit/tsconfig.json b/examples/AqueductUI/src/.svelte-kit/tsconfig.json new file mode 100644 index 0000000..d5b888f --- /dev/null +++ b/examples/AqueductUI/src/.svelte-kit/tsconfig.json @@ -0,0 +1,46 @@ +{ + "compilerOptions": { + "paths": { + "$lib": [ + "../src/lib" + ], + "$lib/*": [ + "../src/lib/*" + ] + }, + "rootDirs": [ + "..", + "./types" + ], + "importsNotUsedAsValues": "error", + "isolatedModules": true, + "preserveValueImports": true, + "lib": [ + "esnext", + "DOM", + "DOM.Iterable" + ], + "moduleResolution": "node", + "module": "esnext", + "target": "esnext", + "ignoreDeprecations": "5.0" + }, + "include": [ + "ambient.d.ts", + "./types/**/$types.d.ts", + "../vite.config.ts", + "../src/**/*.js", + "../src/**/*.ts", + "../src/**/*.svelte", + "../tests/**/*.js", + "../tests/**/*.ts", + "../tests/**/*.svelte" + ], + "exclude": [ + "../node_modules/**", + "./[!ambient.d.ts]**", + "../src/service-worker.js", + "../src/service-worker.ts", + "../src/service-worker.d.ts" + ] +} \ No newline at end of file diff --git a/examples/AqueductUI/src/README.md b/examples/AqueductUI/src/README.md new file mode 100644 index 0000000..5c91169 --- /dev/null +++ b/examples/AqueductUI/src/README.md @@ -0,0 +1,38 @@ +# create-svelte + +Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```bash +# create a new project in the current directory +npm create svelte@latest + +# create a new project in my-app +npm create svelte@latest my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```bash +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```bash +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. diff --git a/examples/AqueductUI/src/package-lock.json b/examples/AqueductUI/src/package-lock.json new file mode 100644 index 0000000..e9a401f --- /dev/null +++ b/examples/AqueductUI/src/package-lock.json @@ -0,0 +1,2495 @@ +{ + "name": "my-project", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "my-project", + "version": "0.0.1", + "dependencies": { + "axios": "^1.5.0", + "hls.js": "^1.4.12" + }, + "devDependencies": { + "@fontsource/fira-mono": "^4.5.10", + "@neoconfetti/svelte": "^1.0.0", + "@sveltejs/adapter-auto": "^2.0.0", + "@sveltejs/kit": "^1.20.4", + "@types/cookie": "^0.5.1", + "autoprefixer": "^10.4.15", + "postcss": "^8.4.29", + "svelte": "^4.0.5", + "svelte-check": "^3.4.3", + "tailwindcss": "^3.3.3", + "tslib": "^2.4.1", + "typescript": "^5.0.0", + "vite": "^4.4.2" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@fontsource/fira-mono": { + "version": "4.5.10", + "resolved": "https://registry.npmjs.org/@fontsource/fira-mono/-/fira-mono-4.5.10.tgz", + "integrity": "sha512-bxUnRP8xptGRo8YXeY073DSpfK74XpSb0ZyRNpHV9WvLnJ7TwPOjZll8hTMin7zLC6iOp59pDZ8EQDj1gzgAQQ==", + "dev": true + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", + "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@neoconfetti/svelte": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@neoconfetti/svelte/-/svelte-1.0.0.tgz", + "integrity": "sha512-SmksyaJAdSlMa9cTidVSIqYo1qti+WTsviNDwgjNVm+KQ3DRP2Df9umDIzC4vCcpEYY+chQe0i2IKnLw03AT8Q==", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.23", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.23.tgz", + "integrity": "sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==", + "dev": true + }, + "node_modules/@sveltejs/adapter-auto": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-2.1.0.tgz", + "integrity": "sha512-o2pZCfATFtA/Gw/BB0Xm7k4EYaekXxaPGER3xGSY3FvzFJGTlJlZjBseaXwYSM94lZ0HniOjTokN3cWaLX6fow==", + "dev": true, + "dependencies": { + "import-meta-resolve": "^3.0.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^1.0.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.24.1.tgz", + "integrity": "sha512-u2FO0q62Se9UZ0g9kXaWYi+54vTK70BKaPScOcx6jLMRou4CUZgDTNKnRhsbJgPMgaLkOH0j3o/fKlZ6jBfgSg==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@sveltejs/vite-plugin-svelte": "^2.4.1", + "@types/cookie": "^0.5.1", + "cookie": "^0.5.0", + "devalue": "^4.3.1", + "esm-env": "^1.0.0", + "kleur": "^4.1.5", + "magic-string": "^0.30.0", + "mime": "^3.0.0", + "sade": "^1.8.1", + "set-cookie-parser": "^2.6.0", + "sirv": "^2.0.2", + "tiny-glob": "^0.2.9", + "undici": "~5.23.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": "^16.14 || >=18" + }, + "peerDependencies": { + "svelte": "^3.54.0 || ^4.0.0-next.0", + "vite": "^4.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.4.5.tgz", + "integrity": "sha512-UJKsFNwhzCVuiZd06jM/psscyNJNDwjQC+qIeb7GBJK9iWeQCcIyfcPWDvbCudfcJggY9jtxJeeaZH7uny93FQ==", + "dev": true, + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^1.0.3", + "debug": "^4.3.4", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.2", + "svelte-hmr": "^0.15.3", + "vitefu": "^0.2.4" + }, + "engines": { + "node": "^14.18.0 || >= 16" + }, + "peerDependencies": { + "svelte": "^3.54.0 || ^4.0.0", + "vite": "^4.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-1.0.4.tgz", + "integrity": "sha512-zjiuZ3yydBtwpF3bj0kQNV0YXe+iKE545QGZVTaylW3eAzFr+pJ/cwK8lZEaRp4JtaJXhD5DyWAV4AxLh6DgaQ==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": "^14.18.0 || >= 16" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^2.2.0", + "svelte": "^3.54.0 || ^4.0.0", + "vite": "^4.0.0" + } + }, + "node_modules/@types/cookie": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.2.tgz", + "integrity": "sha512-DBpRoJGKJZn7RY92dPrgoMew8xCWc2P71beqsjyhEI/Ds9mOyVmBwtekyfhpwFIVt1WrxTonFifiOZ62V8CnNA==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "dev": true + }, + "node_modules/@types/pug": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.6.tgz", + "integrity": "sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/autoprefixer": { + "version": "10.4.15", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.15.tgz", + "integrity": "sha512-KCuPB8ZCIqFdA4HwKXsvz7j6gvSDNhDP7WnUjBleRkKjPdvCmHFuQ77ocavI8FT6NdvlBnE2UFr2H4Mycn8Vew==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.21.10", + "caniuse-lite": "^1.0.30001520", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz", + "integrity": "sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axobject-query": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", + "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "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==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.10", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", + "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001517", + "electron-to-chromium": "^1.4.477", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.11" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dev": true, + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "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, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001532", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001532.tgz", + "integrity": "sha512-FbDFnNat3nMnrROzqrsg314zhqN5LGQ1kyyMk2opcrwGbVGpHRhgCWtAgD5YJUqNAiQ+dklreil/c3Qf1dfCTw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/code-red": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", + "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "@types/estree": "^1.0.1", + "acorn": "^8.10.0", + "estree-walker": "^3.0.3", + "periscopic": "^3.1.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.2.tgz", + "integrity": "sha512-KqFl6pOgOW+Y6wJgu80rHpo2/3H07vr8ntR9rkkFIRETewbf5GaYYcakYfiKz89K+sLsuPkQIZaXDMjUObZwWg==", + "dev": true + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.4.514", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.514.tgz", + "integrity": "sha512-M8LVncPt1Xaw1XLxws6EoJCmY41RXLk87tq6PHvSHDyTYWla3CrEgGlbhC79e8LHyvQ2JTDXx//xzgSixNYcUQ==", + "dev": true + }, + "node_modules/es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/esm-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz", + "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==", + "dev": true + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.6.tgz", + "integrity": "sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "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==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "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": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globalyzer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", + "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", + "dev": true + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/hls.js": { + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.4.12.tgz", + "integrity": "sha512-1RBpx2VihibzE3WE9kGoVCtrhhDWTzydzElk/kyRbEOLnb1WIE+3ZabM/L8BqKFTCL3pUy4QzhXgD1Q6Igr1JA==" + }, + "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, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-meta-resolve": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-3.0.0.tgz", + "integrity": "sha512-4IwhLhNNA8yy445rPjD/lWh++7hMDOml2eHtd58eG7h+qK3EryMuuRbsHGPikCoAgIkkDnckKfWSk2iDla/ejg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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==", + "dev": true, + "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==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-reference": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", + "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", + "dev": true, + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/jiti": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.20.0.tgz", + "integrity": "sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true + }, + "node_modules/magic-string": { + "version": "0.30.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.3.tgz", + "integrity": "sha512-B7xGbll2fG/VjP+SWg4sX3JynwIU0mjoTc6MPpKNuIvftk6u6vqhDnk1R80b8C2GBR6ywqy+1DcKBrevBg+bmw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "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==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "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, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/periscopic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", + "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^3.0.0", + "is-reference": "^3.0.0" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.4.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz", + "integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", + "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", + "dev": true, + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^2.1.1" + }, + "engines": { + "node": ">= 14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", + "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "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, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/rollup": { + "version": "3.29.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.1.tgz", + "integrity": "sha512-c+ebvQz0VIH4KhhCpDsI+Bik0eT8ZFEVZEYw0cGMVqIP8zc+gnwl7iXCamTw7vzv2MeuZFZfdx5JJIq+ehzDlg==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/sander": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", + "integrity": "sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==", + "dev": true, + "dependencies": { + "es6-promise": "^3.1.2", + "graceful-fs": "^4.1.3", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.2" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==", + "dev": true + }, + "node_modules/sirv": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.3.tgz", + "integrity": "sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==", + "dev": true, + "dependencies": { + "@polka/url": "^1.0.0-next.20", + "mrmime": "^1.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sorcery": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz", + "integrity": "sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.14", + "buffer-crc32": "^0.2.5", + "minimist": "^1.2.0", + "sander": "^0.5.0" + }, + "bin": { + "sorcery": "bin/sorcery" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "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", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "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==", + "dev": true, + "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_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.0.tgz", + "integrity": "sha512-kVsdPjDbLrv74SmLSUzAsBGquMs4MPgWGkGLpH+PjOYnFOziAvENVzgJmyOCV2gntxE32aNm8/sqNKD6LbIpeQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@jridgewell/sourcemap-codec": "^1.4.15", + "@jridgewell/trace-mapping": "^0.3.18", + "acorn": "^8.9.0", + "aria-query": "^5.3.0", + "axobject-query": "^3.2.1", + "code-red": "^1.0.3", + "css-tree": "^2.3.1", + "estree-walker": "^3.0.3", + "is-reference": "^3.0.1", + "locate-character": "^3.0.0", + "magic-string": "^0.30.0", + "periscopic": "^3.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/svelte-check": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.5.1.tgz", + "integrity": "sha512-+Zb4iHxAhdUtcUg/WJPRjlS1RJalIsWAe9Mz6G1zyznSs7dDkT7VUBdXc3q7Iwg49O/VrZgyJRvOJkjuBfKjFA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "chokidar": "^3.4.1", + "fast-glob": "^3.2.7", + "import-fresh": "^3.2.1", + "picocolors": "^1.0.0", + "sade": "^1.7.4", + "svelte-preprocess": "^5.0.4", + "typescript": "^5.0.3" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "peerDependencies": { + "svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0" + } + }, + "node_modules/svelte-hmr": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz", + "integrity": "sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==", + "dev": true, + "engines": { + "node": "^12.20 || ^14.13.1 || >= 16" + }, + "peerDependencies": { + "svelte": "^3.19.0 || ^4.0.0" + } + }, + "node_modules/svelte-preprocess": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.0.4.tgz", + "integrity": "sha512-ABia2QegosxOGsVlsSBJvoWeXy1wUKSfF7SWJdTjLAbx/Y3SrVevvvbFNQqrSJw89+lNSsM58SipmZJ5SRi5iw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@types/pug": "^2.0.6", + "detect-indent": "^6.1.0", + "magic-string": "^0.27.0", + "sorcery": "^0.11.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">= 14.10.0" + }, + "peerDependencies": { + "@babel/core": "^7.10.2", + "coffeescript": "^2.5.1", + "less": "^3.11.3 || ^4.0.0", + "postcss": "^7 || ^8", + "postcss-load-config": "^2.1.0 || ^3.0.0 || ^4.0.0", + "pug": "^3.0.0", + "sass": "^1.26.8", + "stylus": "^0.55.0", + "sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "svelte": "^3.23.0 || ^4.0.0-next.0 || ^4.0.0", + "typescript": ">=3.9.5 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "coffeescript": { + "optional": true + }, + "less": { + "optional": true + }, + "postcss": { + "optional": true + }, + "postcss-load-config": { + "optional": true + }, + "pug": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/svelte-preprocess/node_modules/magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/tailwindcss": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz", + "integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.12", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.18.2", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-glob": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", + "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", + "dev": true, + "dependencies": { + "globalyzer": "0.1.0", + "globrex": "^0.1.2" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.23.0.tgz", + "integrity": "sha512-1D7w+fvRsqlQ9GscLBwcAJinqcZGHUKjbOmXdlE/v8BvEGXjeWAax+341q44EuTcHXXnfyKNbKRq4Lg7OzhMmg==", + "dev": true, + "dependencies": { + "busboy": "^1.6.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/vite": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", + "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", + "dev": true, + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.4.tgz", + "integrity": "sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==", + "dev": true, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "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==", + "dev": true + }, + "node_modules/yaml": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz", + "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==", + "dev": true, + "engines": { + "node": ">= 14" + } + } + } +} diff --git a/examples/AqueductUI/src/package.json b/examples/AqueductUI/src/package.json new file mode 100644 index 0000000..5aea814 --- /dev/null +++ b/examples/AqueductUI/src/package.json @@ -0,0 +1,31 @@ +{ + "name": "my-project", + "version": "0.0.1", + "scripts": { + "dev": "vite --host 0.0.0.0 dev", + "build": "vite build", + "preview": "vite --host 0.0.0.0 preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" + }, + "devDependencies": { + "@fontsource/fira-mono": "^4.5.10", + "@neoconfetti/svelte": "^1.0.0", + "@sveltejs/adapter-auto": "^2.0.0", + "@sveltejs/kit": "^1.20.4", + "@types/cookie": "^0.5.1", + "autoprefixer": "^10.4.15", + "postcss": "^8.4.29", + "svelte": "^4.0.5", + "svelte-check": "^3.4.3", + "tailwindcss": "^3.3.3", + "tslib": "^2.4.1", + "typescript": "^5.0.0", + "vite": "^4.4.2" + }, + "type": "module", + "dependencies": { + "axios": "^1.5.0", + "hls.js": "^1.4.12" + } +} diff --git a/examples/AqueductUI/src/postcss.config.js b/examples/AqueductUI/src/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/examples/AqueductUI/src/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/examples/AqueductUI/src/src/app.css b/examples/AqueductUI/src/src/app.css new file mode 100644 index 0000000..bd6213e --- /dev/null +++ b/examples/AqueductUI/src/src/app.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/examples/AqueductUI/src/src/app.d.ts b/examples/AqueductUI/src/src/app.d.ts new file mode 100644 index 0000000..f59b884 --- /dev/null +++ b/examples/AqueductUI/src/src/app.d.ts @@ -0,0 +1,12 @@ +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface Platform {} + } +} + +export {}; diff --git a/examples/AqueductUI/src/src/app.html b/examples/AqueductUI/src/src/app.html new file mode 100644 index 0000000..effe0d0 --- /dev/null +++ b/examples/AqueductUI/src/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/examples/AqueductUI/src/src/components/Modal.svelte b/examples/AqueductUI/src/src/components/Modal.svelte new file mode 100644 index 0000000..6350b9f --- /dev/null +++ b/examples/AqueductUI/src/src/components/Modal.svelte @@ -0,0 +1,68 @@ + + + + (showModal = false)} + on:click|self={() => dialog.close()} +> + +
+ +
+ +
+ + +
+
+ + diff --git a/examples/AqueductUI/src/src/lib/images/github.svg b/examples/AqueductUI/src/src/lib/images/github.svg new file mode 100644 index 0000000..bc5d249 --- /dev/null +++ b/examples/AqueductUI/src/src/lib/images/github.svg @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/examples/AqueductUI/src/src/lib/images/sighthound.svg b/examples/AqueductUI/src/src/lib/images/sighthound.svg new file mode 100644 index 0000000..f59f684 --- /dev/null +++ b/examples/AqueductUI/src/src/lib/images/sighthound.svg @@ -0,0 +1,27 @@ + + + + + + + + diff --git a/examples/AqueductUI/src/src/lib/images/svelte-welcome.png b/examples/AqueductUI/src/src/lib/images/svelte-welcome.png new file mode 100644 index 0000000..fe7d2d6 Binary files /dev/null and b/examples/AqueductUI/src/src/lib/images/svelte-welcome.png differ diff --git a/examples/AqueductUI/src/src/lib/images/svelte-welcome.webp b/examples/AqueductUI/src/src/lib/images/svelte-welcome.webp new file mode 100644 index 0000000..6ec1a28 Binary files /dev/null and b/examples/AqueductUI/src/src/lib/images/svelte-welcome.webp differ diff --git a/examples/AqueductUI/src/src/routes/+layout.svelte b/examples/AqueductUI/src/src/routes/+layout.svelte new file mode 100644 index 0000000..835a393 --- /dev/null +++ b/examples/AqueductUI/src/src/routes/+layout.svelte @@ -0,0 +1,54 @@ + + +
+
+ +
+ +
+ + +
+ + diff --git a/examples/AqueductUI/src/src/routes/+page.svelte b/examples/AqueductUI/src/src/routes/+page.svelte new file mode 100644 index 0000000..e5f78dd --- /dev/null +++ b/examples/AqueductUI/src/src/routes/+page.svelte @@ -0,0 +1,138 @@ + + + + Home + + + +
+

+ + + + Welcome + + +

+ +
+ +
+ + + {#each Object.keys(localUrls) as key} + +
+ +
+ {/each} +
+ + diff --git a/examples/AqueductUI/src/src/routes/+page.ts b/examples/AqueductUI/src/src/routes/+page.ts new file mode 100644 index 0000000..a72419a --- /dev/null +++ b/examples/AqueductUI/src/src/routes/+page.ts @@ -0,0 +1,3 @@ +// since there's no dynamic data here, we can prerender +// it so that it gets served as a static asset in production +export const prerender = true; diff --git a/examples/AqueductUI/src/src/routes/Header.svelte b/examples/AqueductUI/src/src/routes/Header.svelte new file mode 100644 index 0000000..6ba2ebf --- /dev/null +++ b/examples/AqueductUI/src/src/routes/Header.svelte @@ -0,0 +1,151 @@ + + +
+
+ + Sighthound + +
+ + + +
+ + GitHub + +
+
+ + diff --git a/examples/AqueductUI/src/src/routes/about/+page.svelte b/examples/AqueductUI/src/src/routes/about/+page.svelte new file mode 100644 index 0000000..e0b48a8 --- /dev/null +++ b/examples/AqueductUI/src/src/routes/about/+page.svelte @@ -0,0 +1,8 @@ + + About + + + +
+

About Aqueduct

+
diff --git a/examples/AqueductUI/src/src/routes/about/+page.ts b/examples/AqueductUI/src/src/routes/about/+page.ts new file mode 100644 index 0000000..e739ef4 --- /dev/null +++ b/examples/AqueductUI/src/src/routes/about/+page.ts @@ -0,0 +1,9 @@ +import { dev } from '$app/environment'; + +// we don't need any JS on this page, though we'll load +// it in dev so that we get hot module replacement +export const csr = dev; + +// since there's no dynamic data here, we can prerender +// it so that it gets served as a static asset in production +export const prerender = true; diff --git a/examples/AqueductUI/src/src/routes/mcp/+page.svelte b/examples/AqueductUI/src/src/routes/mcp/+page.svelte new file mode 100644 index 0000000..2f6013b --- /dev/null +++ b/examples/AqueductUI/src/src/routes/mcp/+page.svelte @@ -0,0 +1,17 @@ + + + + Pipelines + + + +
+

Media Control Plane

+
+ + + diff --git a/examples/AqueductUI/src/src/routes/mcp/+page.ts b/examples/AqueductUI/src/src/routes/mcp/+page.ts new file mode 100644 index 0000000..9a8dc7a --- /dev/null +++ b/examples/AqueductUI/src/src/routes/mcp/+page.ts @@ -0,0 +1,5 @@ +import { dev } from '$app/environment'; + +// we don't need any JS on this page, though we'll load +// it in dev so that we get hot module replacement +export const csr = dev; \ No newline at end of file diff --git a/examples/AqueductUI/src/src/routes/mcp/MCP.svelte b/examples/AqueductUI/src/src/routes/mcp/MCP.svelte new file mode 100644 index 0000000..7599681 --- /dev/null +++ b/examples/AqueductUI/src/src/routes/mcp/MCP.svelte @@ -0,0 +1,281 @@ + + +
+

+ Connecting to MCP server: {mcpUrl} +

+ + + + + + {#if errorMessage} +
{errorMessage}
+ {/if} + +

+ List of MCP Sources +

+ + {#if Object.keys(sourceStats).length} +
+ + + + + + + + + + + + + + {#each Object.keys(sourceStats) as sourceId} + + + + + + + + + {/each} + +
Source IDNumber of ImagesNumber of VideosSizeLatest ImageLive ViewDelete source
{sourceId}{sourceStats[sourceId].numberOfImages}{sourceStats[sourceId].numberOfVideos}{formatBytes(sourceStats[sourceId].fileSize)} + {#if sourceStats[sourceId].numberOfImages !== 0} + View + {/if} + + {#if sourceStats[sourceId].numberOfVideos !== 0} + + {/if} + + +
+
+ {:else} +
No sources found
+ {/if} +
+ +
+{#if displayHls} +

+ Live view +

+ + + +
+ +{/if} + + diff --git a/examples/AqueductUI/src/src/routes/pipelines/+page.svelte b/examples/AqueductUI/src/src/routes/pipelines/+page.svelte new file mode 100644 index 0000000..9630c39 --- /dev/null +++ b/examples/AqueductUI/src/src/routes/pipelines/+page.svelte @@ -0,0 +1,16 @@ + + + + Pipelines + + + +
+

Pipelines

+
+ + + diff --git a/examples/AqueductUI/src/src/routes/pipelines/+page.ts b/examples/AqueductUI/src/src/routes/pipelines/+page.ts new file mode 100644 index 0000000..9a8dc7a --- /dev/null +++ b/examples/AqueductUI/src/src/routes/pipelines/+page.ts @@ -0,0 +1,5 @@ +import { dev } from '$app/environment'; + +// we don't need any JS on this page, though we'll load +// it in dev so that we get hot module replacement +export const csr = dev; \ No newline at end of file diff --git a/examples/AqueductUI/src/src/routes/pipelines/ListPipelines.svelte b/examples/AqueductUI/src/src/routes/pipelines/ListPipelines.svelte new file mode 100644 index 0000000..add19a1 --- /dev/null +++ b/examples/AqueductUI/src/src/routes/pipelines/ListPipelines.svelte @@ -0,0 +1,196 @@ + + +
+ + + + {#if errorMessage} +
{errorMessage}
+ {/if} + +

+ List of Pipelines +

+ {#if $pipelines} +
+ + + + + + + + + + + + + {#each Object.keys($pipelines) as sourceId} + + + + + + + + + {/each} + +
Source IDURLStatusLast UpdateActionDelete
{sourceId}{$pipelines[sourceId].data.parameters + .VIDEO_IN}{$pipelines[sourceId].status}{timeSince( + $pipelines[sourceId].lastUpdate + )} + + + +
+
+ {/if} +
+ + diff --git a/examples/AqueductUI/src/src/routes/start/+page.svelte b/examples/AqueductUI/src/src/routes/start/+page.svelte new file mode 100644 index 0000000..d078227 --- /dev/null +++ b/examples/AqueductUI/src/src/routes/start/+page.svelte @@ -0,0 +1,209 @@ + + + + start + + + +
+ +

+ Starting an Aqueduct Pipeline +

+ +
+ +
+ + + + + + +
+ + {#if loading} +
+ + Loading... +
+ {/if} +
+ +

Extra SIO Parameters

+{#each extra_parameters as extra, index} +
+
+ + + + +
+ +
+{/each} + + + +

+ Message +

+ +

{modalMessage}

+
diff --git a/examples/AqueductUI/src/src/routes/start/+page.ts b/examples/AqueductUI/src/src/routes/start/+page.ts new file mode 100644 index 0000000..9d8304e --- /dev/null +++ b/examples/AqueductUI/src/src/routes/start/+page.ts @@ -0,0 +1,5 @@ +import { dev } from '$app/environment'; + +// we don't need any JS on this page, though we'll load +// it in dev so that we get hot module replacement +export const csr = dev; diff --git a/examples/AqueductUI/src/src/routes/styles.css b/examples/AqueductUI/src/src/routes/styles.css new file mode 100644 index 0000000..cbc8889 --- /dev/null +++ b/examples/AqueductUI/src/src/routes/styles.css @@ -0,0 +1,107 @@ +@import '@fontsource/fira-mono'; + +:root { + --font-body: Arial, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, + Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + --font-mono: 'Fira Mono', monospace; + --color-bg-0: rgb(202, 216, 228); + --color-bg-1: hsl(209, 36%, 86%); + --color-bg-2: hsl(224, 44%, 95%); + --color-theme-1: #4f60dc; + --color-theme-2: #4075a6; + --color-text: rgba(0, 0, 0, 0.7); + --column-width: 42rem; + --column-margin-top: 4rem; + font-family: var(--font-body); + color: var(--color-text); +} + +body { + min-height: 100vh; + margin: 0; + background-attachment: fixed; + background-color: var(--color-bg-1); + background-size: 100vw 100vh; + background-image: radial-gradient( + 50% 50% at 50% 50%, + rgba(255, 255, 255, 0.75) 0%, + rgba(255, 255, 255, 0) 100% + ), + linear-gradient(180deg, var(--color-bg-0) 0%, var(--color-bg-1) 15%, var(--color-bg-2) 50%); +} + +h1, +h2, +p { + font-weight: 400; +} + +p { + line-height: 1.5; +} + +a { + color: var(--color-theme-1); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +h1 { + font-size: 2rem; + text-align: center; +} + +h2 { + font-size: 1rem; +} + +pre { + font-size: 16px; + font-family: var(--font-mono); + background-color: rgba(255, 255, 255, 0.45); + border-radius: 3px; + box-shadow: 2px 2px 6px rgb(255 255 255 / 25%); + padding: 0.5em; + overflow-x: auto; + color: var(--color-text); +} + +.text-column { + display: flex; + max-width: 48rem; + flex: 0.6; + flex-direction: column; + justify-content: center; + margin: 0 auto; +} + +input, +button { + font-size: inherit; + font-family: inherit; +} + +button:focus:not(:focus-visible) { + outline: none; +} + +@media (min-width: 720px) { + h1 { + font-size: 2.4rem; + } +} + +.visually-hidden { + border: 0; + clip: rect(0 0 0 0); + height: auto; + margin: 0; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + white-space: nowrap; +} diff --git a/examples/AqueductUI/src/src/stores.ts b/examples/AqueductUI/src/src/stores.ts new file mode 100644 index 0000000..2aa44cf --- /dev/null +++ b/examples/AqueductUI/src/src/stores.ts @@ -0,0 +1,47 @@ +import { writable, type Writable } from 'svelte/store'; + +interface SioParameters { + VIDEO_IN: string; + sourceId: string; + recordTo: string; + imageSaveDir: string; + amqpHost: string; + amqpPort: string; + amqpExchange: string; + amqpUser: string; + amqpPassword: string; + amqpErrorOnFailure: string; + [extraParameter: string]: any; + } + + interface PipelineData { + pipeline: string; + parameters: SioParameters; + } + + interface Pipeline { + data: PipelineData; + status: string; + lastStatusUpdate: number; + lastUpdate: number; + created: number; + source: string; + } + + +interface Pipelines { + [key: string]: Pipeline; +} + +export const pipelines: Writable = writable({}); + +const defaultDeviceUrl = ""; +export const deviceUrl = writable(defaultDeviceUrl); +export const aqueductUrl = writable(`http://${defaultDeviceUrl}:8888`); +export const mcpUrl = writable(`http://${defaultDeviceUrl}:9097`); + +export function updateDeviceUrl(url: string) { + deviceUrl.set(url); + aqueductUrl.set(`http://${url}:8888`); + mcpUrl.set(`http://${url}:9097`); +} \ No newline at end of file diff --git a/examples/AqueductUI/src/static/favicon.png b/examples/AqueductUI/src/static/favicon.png new file mode 100644 index 0000000..be4ff98 Binary files /dev/null and b/examples/AqueductUI/src/static/favicon.png differ diff --git a/examples/AqueductUI/src/static/robots.txt b/examples/AqueductUI/src/static/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/examples/AqueductUI/src/static/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/examples/AqueductUI/src/svelte.config.js b/examples/AqueductUI/src/svelte.config.js new file mode 100644 index 0000000..1cf26a0 --- /dev/null +++ b/examples/AqueductUI/src/svelte.config.js @@ -0,0 +1,18 @@ +import adapter from '@sveltejs/adapter-auto'; +import { vitePreprocess } from '@sveltejs/kit/vite'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://kit.svelte.dev/docs/integrations#preprocessors + // for more information about preprocessors + preprocess: vitePreprocess(), + + kit: { + // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. + // If your environment is not supported or you settled on a specific environment, switch out the adapter. + // See https://kit.svelte.dev/docs/adapters for more information about adapters. + adapter: adapter() + } +}; + +export default config; diff --git a/examples/AqueductUI/src/tailwind.config.js b/examples/AqueductUI/src/tailwind.config.js new file mode 100644 index 0000000..13207cc --- /dev/null +++ b/examples/AqueductUI/src/tailwind.config.js @@ -0,0 +1,9 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{html,js,svelte,ts}'], + theme: { + extend: {}, + }, + plugins: [], +} + diff --git a/examples/AqueductUI/src/tsconfig.json b/examples/AqueductUI/src/tsconfig.json new file mode 100644 index 0000000..6ae0c8c --- /dev/null +++ b/examples/AqueductUI/src/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true + } + // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} diff --git a/examples/AqueductUI/src/vite.config.ts b/examples/AqueductUI/src/vite.config.ts new file mode 100644 index 0000000..bbf8c7d --- /dev/null +++ b/examples/AqueductUI/src/vite.config.ts @@ -0,0 +1,6 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()] +}); diff --git a/examples/SIORtspOutput/src/RTSPStream.py b/examples/SIORtspOutput/src/RTSPStream.py index be08679..384a7c6 100644 --- a/examples/SIORtspOutput/src/RTSPStream.py +++ b/examples/SIORtspOutput/src/RTSPStream.py @@ -54,6 +54,7 @@ def clear_frame(self, color=None): self.frame = np.full((self.height, self.width, self.channels), color, np.uint8) else: self.frame = np.zeros((self.height, self.width, self.channels), np.uint8) + self.write_text("No frame", location="center", color=(0, 0, 255), font_scale=2, thickness=3) def resize_frame(self): # It is better to change the resolution of the camera diff --git a/examples/lib/Aqueduct.py b/examples/lib/Aqueduct.py new file mode 100644 index 0000000..576ca7f --- /dev/null +++ b/examples/lib/Aqueduct.py @@ -0,0 +1,391 @@ +import pika +import json +import time +import glob +import logging +import os +from datetime import datetime +from threading import Thread +from tabulate import tabulate + +logger = logging.getLogger("aqueduct") + +def time_ago(time): + """ + Get a datetime object or a int() Epoch timestamp and return a + pretty string like 'an hour ago', 'Yesterday', '3 months ago', + 'just now', etc + Modified from: http://stackoverflow.com/a/1551394/141084 + """ + + if time == 0: + return 'Never' + now = datetime.now() + if type(time) is int: + diff = now - datetime.fromtimestamp(time) + elif type(time) is float: + diff = now - datetime.fromtimestamp(time) + elif isinstance(time,datetime): + diff = now - time + else: + raise ValueError('invalid date %s of type %s' % (time, type(time))) + second_diff = diff.seconds + day_diff = diff.days + + if day_diff < 0: + return 'error' + + if day_diff == 0: + if second_diff < 10: + return "just now" + if second_diff < 60: + return str(round(second_diff,2)) + " seconds ago" + if second_diff < 120: + return "a minute ago" + if second_diff < 3600: + return str( round(second_diff / 60 ,2)) + " minutes ago" + if second_diff < 7200: + return "an hour ago" + if second_diff < 86400: + return str( round(second_diff / 3600 ,2)) + " hours ago" + if day_diff == 1: + return "Yesterday" + if day_diff < 7: + return str(round(day_diff, 2)) + " days ago" + if day_diff < 31: + return str(round(day_diff/7,2)) + " weeks ago" + if day_diff < 365: + return str(round(day_diff/30, 2)) + " months ago" + return str(round(day_diff/365,2)) + " years ago" + +class Pipelines: + def __init__(self, path, pipelinesDBFile, publisher, _logger): + self.path = path + self.pipelinesDBFile = pipelinesDBFile + self.logger = _logger.getChild('pipelines') + self.publisher = publisher + self.logger.info('Reading pipelines from: %s', self.path) + if self.path != "": + if not os.path.exists(self.path): + os.makedirs(self.path) + if not os.path.isdir(self.path): + self.logger.getChild("init").error('Pipelines path does not exist: %s', self.path) + raise Exception('Pipelines path does not exist: %s', self.path) + else: + self.logger.getChild("init").warning('Pipelines path is empty, will not load any pipelines or save pipelines data') + + # Check if db file exists + if not os.path.isfile(self.pipelinesDBFile): + self.logger.getChild("init").info('Creating db file: %s', self.pipelinesDBFile) + with open(self.pipelinesDBFile, 'w') as f: + f.write('{}') + + def getDB(self): + if self.path == "": + return {} + with open(self.pipelinesDBFile, 'r') as f: + data = json.load(f) + return data + + def writeDB(self, data): + if self.path == "": + return + with open(self.pipelinesDBFile, 'w') as f: + json.dump(data, f, indent=4) + + def handle_message(self, watch_mode, method, props, body): + routing_key = method.routing_key + print(f" [x] Received {routing_key}: {body}") + try: + message = json.loads(body) + if routing_key.startswith("aqueduct.status"): + self.logger.debug(f"Pipeline '{message['sourceId']}' status: {message['cause']}") + self.updateStatusPipeline(message['sourceId'], message['cause']) + if watch_mode: + self.print_pipelines_db(datetime.now().strftime("%H:%M:%S"), self.getDB(), clear=True) + except Exception as e: + print(e) + def anypipe_message(self, method, props, body, json_file): + routing_key = method.routing_key + print(f" [x] Received {routing_key}: {body}") + try: + message = json.loads(body) + if json_file != "": + # Append message to json file + with open(json_file, 'a') as f: + f.write(json.dumps(message) + "\n") + except Exception as e: + print(e) + + def exists(self, id): + pipelines_db = self.getDB() + return id in pipelines_db + + def ps(self): + if self.path == "": + self.logger.getChild("ps").warning('Pipelines path is empty, will not load any pipelines or save pipelines data') + return + self.updateDBFromDisk() + data = self.getDB() + self.print_pipelines_db("Pipelines", data) + + + def updateStatusPipeline(self, id, status): + if self.path == "": + return + pipelines_db = self.getDB() + if id in pipelines_db: + self.logger.info(f"Pipeline {id}, changed status from {pipelines_db[id]['status']} to {status}") + pipelines_db[id]["status"] = status + pipelines_db[id]['lastStatusUpdate'] = int(datetime.now().timestamp()) + else: + self.logger.error(f"Pipeline {id} not found in db") + self.writeDB(pipelines_db) + + def addOrUpdatePipeline(self, id, source, data): + source = os.path.basename(source).replace('.json', '') + if self.path == "": + logger.warn(f"Cannot add or update pipeline {id}") + pipelines_db = self.getDB() + lastUpdate = int(datetime.now().timestamp()) + if id in pipelines_db: + if source != pipelines_db[id]['source']: + self.logger.error(f"Error while updating {id}, source was added by another file {pipelines_db[id]['source']}, but found in {source}") + pipelines_db[id]["data"] = data + pipelines_db[id]["lastUpdate"] = lastUpdate + else: + status = "loaded" + lastStatusUpdate = 0 + created = int(datetime.now().timestamp()) + pipelines_db[id] = {"data": data, "status": status, "lastStatusUpdate": lastStatusUpdate, "lastUpdate": lastUpdate, "created": created, "source": source} + + self.writeDB(pipelines_db) + + def print_pipelines_db(self, title, pipelines_db, clear=False): + if self.path == "": + return [] + if clear: + os.system('cls' if os.name in ('nt', 'dos') else 'clear') + print(title) + table = [['Id', 'Status', 'Last Status Update', 'Last Update', 'Created']] + for id, pipeline in pipelines_db.items(): + created_str = datetime.fromtimestamp(pipeline['created']).strftime('%m/%d/%Y, %H:%M:%S') + lastStatusUpdate_str = time_ago(pipeline['lastStatusUpdate']) + lastUpdate_str = datetime.fromtimestamp(pipeline['lastUpdate']).strftime('%m/%d/%Y, %H:%M:%S') + table.append([id, pipeline['status'], lastStatusUpdate_str, lastUpdate_str, created_str]) + print(tabulate(table)) + + def updateDBFromDisk(self): + for aqueduct_file in glob.glob(self.path + '/*.json'): + try: + with open(aqueduct_file) as f: + data = json.load(f) + for pipeline in data: + self.addOrUpdatePipeline(pipeline, aqueduct_file, data[pipeline]) + except Exception as e: + self.logger.getChild("updateDBFromDisk").error('Error loading pipeline: %s', aqueduct_file) + print(e) + + def get(self, id): + if self.path == "": + return None + for pipeline in glob.glob(self.path + '/*.json'): + try: + with open(pipeline) as f: + data = json.load(f) + if id in data: + return data[id] + except: + self.logger.getChild("get").warning('Failed to load pipeline: %s', pipeline) + return None + + def runFolderWatch(self): + if self.path == "": + self.logger.getChild("runFolder").warning('Pipelines path is empty, will not load any pipelines or save pipelines data') + return + self.updateDBFromDisk() + + for id, data in self.getDB().items(): + logger.info("Running pipeline: %s", id) + self.runPipeline(id, data["data"]) + + def run_from_file(self, source_file): + with open(source_file) as f: + data = json.load(f) + self.run(data, source_file) + + def run(self, pipelines, source="unknown"): + logger = self.logger.getChild("run") + print("Running pipelines from: ", source) + for id, pipeline in pipelines.items(): + logger.info("Running pipeline: %s", id) + self.addOrUpdatePipeline(id, source, pipeline) + self.runPipeline(id, pipeline) + + def runPipeline(self, id, pipeline): + logger.info("Running pipeline: %s", id) + msg = pipeline + msg["command"] = "execute" + msg["sourceId"] = id + self.publisher.publish(json.dumps(msg), 'execute', "." + id) + self.updateStatusPipeline(id, "execute_sent") + + def stopPipeline(self, id): + logger.info("Stopping pipeline: %s", id) + msg = {"command": "stop", "sourceId": id} + self.publisher.publish(json.dumps(msg), 'execute', "." + id) + self.updateStatusPipeline(id, "stop_sent") + + def deletePipeline(self, id): + logger.info("Deleting pipeline: %s", id) + try: + self.stopPipeline(id) + except Exception as e: + print(e) + self.getDB().pop(id, None) + self.writeDB(self.getDB()) + + def status(self, id): + pipelines_db = self.getDB() + if id in pipelines_db: + return pipelines_db[id]['status'] + else: + return "not found" + + def waitPipeline(self, id, waitFor, timeout): + logger.info("Waiting for pipeline: %s, timeout: %d", id, timeout) + t0 = time.time() + while True: + if timeout > 0 and time.time() - t0 > timeout: + logger.error("Timeout waiting for pipeline: %s", id) + return False + pipelines_db = self.getDB() + if id not in pipelines_db: + logger.error("Wait: Pipeline not found: %s", id) + return False + if pipelines_db[id]['status'] in waitFor: + logger.info("Wait: Pipeline %s: %s", pipelines_db[id]['status'], id ) + return True + else: + logger.info("Wait pipeline '%s' status: %s", id, pipelines_db[id]['status']) + time.sleep(1) + + def stop(self, pipeline): + logger = self.logger.getChild("stop") + try: + with open(pipeline) as f: + data = json.load(f) + for id, pipeline in data.items(): + logger.info("Stopping pipeline: %s", id) + self.stopPipeline(id) + except: + self.stopPipeline(pipeline) + + def wait(self, pipeline, waitFor, timeout): + logger = self.logger.getChild("wait") + if os.path.isfile(pipeline): + with open(pipeline) as f: + data = json.load(f) + for id, pipeline in data.items(): + logger.info("Waiting for pipeline: %s", id) + return self.waitPipeline(id, waitFor, timeout) + else: + return self.waitPipeline(pipeline, waitFor, timeout) + + def delete(self, pipeline): + logger = self.logger.getChild("delete") + if os.path.isfile(pipeline): + with open(pipeline) as f: + data = json.load(f) + for id, pipeline in data.items(): + logger.info("Deleting pipeline: %s", id) + self.deletePipeline(id) + else: + self.deletePipeline(pipeline) + +class AqueductAMQP: + def __init__(self, name, amqpHost, amqpPort, amqpUsername, amqpPassword, logger): + self.logger = logger.getChild("aqueduct-amqp").getChild(name) + self.logger.info(f"Connecting to AMQP on {amqpHost}:{amqpPort} with user: {amqpUsername}...") + self._params = pika.connection.ConnectionParameters( + host=amqpHost, + port=amqpPort, + virtual_host='/', + credentials=pika.credentials.PlainCredentials(amqpUsername, amqpPassword)) + self._conn = None + self._channel = None + + + def connect(self): + if not self._conn: + self.logger.info('Connecting to AMQP instance: %s:%s', self._params.host, self._params.port) + else: + self.logger.info(f'Reconnecting to AMQP instance: {self._params.host}:{self._params.port} ...') + if not self._conn.is_closed: + self._conn.close() + + self._conn = pika.BlockingConnection(self._params) + self._channel = self._conn.channel() + self._channel.exchange_declare(exchange="aqueduct", exchange_type='topic', durable=True) + + + + def _choose_routing_key(self, topic): + if topic == "execute": + return 'aqueduct.execute.default' + elif topic == "control": + return 'aqueduct.control.default' + elif topic == "status": + return 'aqueduct.status.#' + elif topic == "everything": + return '#' + else: + raise Exception("Unknown topic: " + topic) + + def _publish(self, msg, routing_key): + if self._channel is None: + raise Exception('Not connected to queue') + self.logger.info('publishing message: \'%s\' to exchange: "%s" and routing_key: "%s"', msg, 'aqueduct', routing_key) + self._channel.basic_publish(exchange='aqueduct', + routing_key=routing_key, + properties=pika.BasicProperties(content_type='application/json'), + body=msg) + self.logger.debug('message sent: %s', msg) + + def publish(self, msg, topic=None, suffix=""): + """Publish msg, reconnecting if necessary.""" + routing_key = self._choose_routing_key(topic) + suffix + try: + self._publish(msg, routing_key) + except (pika.exceptions.ConnectionClosed, pika.exceptions.StreamLostError): + self.connect() + self._publish(msg, routing_key) + + def subscribe(self, callback, exchange="aqueduct", topic=None): + self.logger.debug('subscribing to "%s":"%s"', exchange, self._choose_routing_key(topic)) + if self._channel is None: + self.logger.error('channel is not initialized') + return + queue = self._channel.queue_declare(queue='', exclusive=True) + self._channel.queue_bind(exchange=exchange, + queue=queue.method.queue, + routing_key=self._choose_routing_key(topic)) + self._channel.basic_consume(queue=queue.method.queue, + on_message_callback=callback, + auto_ack=True) + self._channel.start_consuming() + + def close(self): + if self._conn and self._conn.is_open: + self.logger.debug('closing queue connection') + self._conn.close() + +def subscribe(publisher, callback, background=True, exchange="aqueduct", topic='status'): + if background: + T = Thread(target = publisher.subscribe, args = (callback, exchange, topic)) + # change T to daemon + T.setDaemon(True) + # starting of Thread T + T.start() + else: + publisher.subscribe(callback, exchange, topic) \ No newline at end of file diff --git a/examples/lib/SIODrawer.py b/examples/lib/SIODrawer.py index 164071f..1131469 100644 --- a/examples/lib/SIODrawer.py +++ b/examples/lib/SIODrawer.py @@ -17,6 +17,7 @@ def clear_frame(self, color=None): self.frame = np.full((self.height, self.width, self.channels), color, np.uint8) else: self.frame = np.zeros((self.height, self.width, self.channels), np.uint8) + self.write_text("No frame", location="center", color=(0, 0, 255), font_scale=2, thickness=3) def save_frame(self, filename): cv2.imwrite(filename, self.frame) @@ -137,11 +138,13 @@ def stream_callback(self, data): self.last_sequence = event.get("sequence") break if image_found: + print(f"- Received SIO data with images") try: self.set_current_frame(self.mcp.get_image(source_id, image_found)) except Exception as e: print(f"Caught exception {e}") else: + print(f"- Received SIO data without images") return self.draw_sio_data(data) diff --git a/scripts/sh-services b/scripts/sh-services index b8980e7..f1a9b91 100755 --- a/scripts/sh-services +++ b/scripts/sh-services @@ -9,6 +9,8 @@ SH_BASE="$(realpath "${SCRIPT_PATH}"/../..)" export SH_BASE SERVICES_PATH="$(realpath "${SCRIPT_PATH}"/../services)" export SERVICES_PATH +EXAMPLES_PATH="$(realpath "${SCRIPT_PATH}"/../examples)" +export EXAMPLES_PATH MEDIA_PATH="${SH_BASE}"/media export MEDIA_PATH DB_PATH="${SH_BASE}"/db @@ -62,28 +64,28 @@ check_service_up() { local container_ids local container_id local state - + # Get container IDs container_ids=$(${DOCKER_COMPOSE} -f "${service_full_path}/docker-compose.yml" ps -a -q) - + # Check if there are no containers if [ -z "$container_ids" ]; then echo "No ${service} containers are found" return 1 fi - + # Loop through each container ID for container_id in $container_ids; do # Get the state of the container state=$(docker inspect --format='{{.State.Status}}' "$container_id") - + # Check if the state is not 'running' if [ "$state" != "running" ]; then echo "Container '$container_id; from ${service} is not up" return 1 fi done - + # If the function reaches this point, all containers are running echo "All ${service} containers are up" return 0 @@ -206,15 +208,11 @@ EOF SH_NODE=false fi fi - + echo "SH_ARCH=-r${L4TBASE_VERSION}-arm64v8" >>"${SERVICES_PATH}"/conf/0010-arch.env echo "SH_MACHINE=${SH_MACHINE}" >>"${SERVICES_PATH}"/conf/0010-arch.env echo "SH_NODE=${SH_NODE}" >>"${SERVICES_PATH}"/conf/0010-arch.env echo "SH_SERIAL=${SH_SERIAL}" >>"${SERVICES_PATH}"/conf/0010-arch.env - # TODO: remove this hardcoded env var - if [[ $SH_NODE == "false" ]]; then - echo "SIO_ENABLE_NVMPI=0" >>"${SERVICES_PATH}"/conf/0010-arch.env - fi else echo "Unknown architecture: $(uname -m)" exit 1 @@ -269,7 +267,7 @@ function set_text_editor() { return 0 fi REPLY= - done + done } function remove_comments_and_duplicates() { local file=$1 @@ -277,7 +275,7 @@ function remove_comments_and_duplicates() { rm "${file}.bkp" # This awk command reads the contents of the file specified by the variable 'file' and stores each line in an array 'a'. # It then prints the contents of the array in reverse order, removing any duplicate lines based on the first field (before the '=' character) using the 'seen' array. - awk '{a[i++]=$0} END {for (j=i-1; j>=0;) print a[j--] }' "${file}" | awk -F= '!seen[$1]++' | awk '{a[i++]=$0} END {for (j=i-1; j>=0;) print a[j--] }' > "${file}.tmp" + awk '{a[i++]=$0} END {for (j=i-1; j>=0;) print a[j--] }' "${file}" | awk -F= '!seen[$1]++' | awk '{a[i++]=$0} END {for (j=i-1; j>=0;) print a[j--] }' > "${file}.tmp" mv "${file}.tmp" "${file}" } function show() { @@ -298,6 +296,21 @@ function config() { "${service_full_path}"/config.sh } +function start_example() { + local example=$1 + local example_full_path="${EXAMPLES_PATH}/${example}" + if [ ! -f "${example_full_path}/docker-compose.yml" ]; then + echo "No docker-compose.yml file found for ${example}" + return + fi + echo "Starting ${example}" + cd "${example_full_path}" || exit 1 + ${DOCKER_COMPOSE} build + ${DOCKER_COMPOSE} up -d + sleep 1 + ${DOCKER_COMPOSE} ps +} + function edit() { local service=$1 local service_full_path="${SERVICES_PATH}/${service}" @@ -436,7 +449,7 @@ function create_network() { function depends() { local service=$1 local service_full_path="${SERVICES_PATH}/${service}" - + # Check if the folder contains the "depends" file if [ -f "${service_full_path}/depends" ]; then # Read the contents of the "depends" file @@ -520,6 +533,10 @@ function depends() { return 0 } +function start(){ + up "$@" +} + function up() { local service=$1 local service_full_path="${SERVICES_PATH}/${service}" @@ -550,6 +567,10 @@ function compose_up() { cd "${SERVICES_PATH}" || exit 1 } +function stop () { + down "$@" +} + function down() { local service=$1 echo "Stopping ${service}" @@ -645,14 +666,26 @@ select_live555_video() { if [[ $file_path == *.mkv ]]; then if [ -f "$file_path" ]; then - mkdir -p "${SERVICES_PATH}/live555/test-data" - cp "$file_path" "${SERVICES_PATH}/live555/test-data/my-video.mkv" + mkdir -p "${MEDIA_PATH}/input/video/live555" + cp "$file_path" "${MEDIA_PATH}/input/video/live555/my-video.mkv" echo "Selected file: $file_path" + ls -la "${MEDIA_PATH}"/input/video/live555/my-video.mkv + return 0 else - echo "Invalid file path or file does not exist: $file_path" + echo "ERROR: Invalid file path or file does not exist: $file_path" + return 1 fi else - echo "Invalid file format. Please select an MKV file." + if [[ $file_path == "" ]]; then + echo "Contents of ${MEDIA_PATH}/input/video/live555:" + find "${MEDIA_PATH}/input/video/live555" + echo "No file selected. Skipping...." + sleep 3 + return 0 + else + echo "Invalid file format. Please select a valid MKV file and try again." + return 1 + fi fi } @@ -753,7 +786,7 @@ function interactively_apply_command() { break fi interactively_handle_service "$service" - else + else echo "Invalid option $REPLY" fi REPLY= @@ -793,9 +826,12 @@ function apply_to_services() { for service in "${SERVICES[@]}"; do # Check if service in AVAILABLE_SERVICES if element_in_list "${service}" "${AVAILABLE_SERVICES[@]}"; then - if [ "${service}" == " " ]; then + if [[ "${service}" == " " ]]; then continue fi + if [[ ! -f "${SERVICES_PATH}/${service}/.env" ]]; then + merge "${service}" + fi # Some commands should not print the service if [ "${COMMAND}" != "status" ] ; then echo "" @@ -825,10 +861,16 @@ function apply() { echo "File not found: $filename" exit 1 fi + set -e while IFS= read -r line || [[ -n "$line" ]] do echo "- $line" $line + rt=$? + if [ $rt -ne 0 ]; then + echo "Error executing command: $line : $rt" + exit 1 + fi done < "$filename" wait echo "Done" @@ -844,6 +886,7 @@ LIST_OF_COMMANDS=" apply - Apply a configuration file edit [service(s)] - Modify configurations or settings of a service config [service(s)] - Runs the configuration script of a service if available select_example [service(s)] - Select an example configuration for a service. E.g: select_example sio aqueduct + start_example [example] - Start an example configuration. E.g: start_example AqueductAPI enable [service(s)] - Turn on a service disable [service(s)] - Turn off a service depends [service(s)] - Show the dependencies of a service and it's status @@ -857,10 +900,10 @@ LIST_OF_COMMANDS=" apply - Apply a configuration file COMMAND=$1 shift 1 || true case $COMMAND in -"select_example" | "select_live555_video" | "apply" | "test_rtsp_stream") +"select_example" | "set_example" | "select_live555_video" | "apply" | "test_rtsp_stream" | "start_example") "$COMMAND" $@ ;; -"merge" | "up" | "down" | "restart" | "enable" | "disable" | "depends" | "edit" | "config" | "show" | "status") +"merge" | "up" | "start" | "down" | "stop" | "restart" | "enable" | "disable" | "depends" | "edit" | "config" | "show" | "status") apply_to_services "$COMMAND" $@ ;; "clean_media" | "clean_logs" | "clean_rabbitmq" | "remove_orphans" | "ps" | "license") diff --git a/services/live555/conf/default.env b/services/live555/conf/default.env index cd604ca..3208940 100644 --- a/services/live555/conf/default.env +++ b/services/live555/conf/default.env @@ -1,4 +1,4 @@ LIVE555_DOCKER_IMAGE=us-central1-docker.pkg.dev/ext-edge-analytics/docker/live555 -LIVE555_DOCKER_TAG=2.0.3-examples +LIVE555_DOCKER_TAG=2.0.4-examples LIVE555_CONTAINER_NAME=live555 SERVICE_GROUP=-dev \ No newline at end of file diff --git a/services/live555/docker-compose.yml b/services/live555/docker-compose.yml index c0b7c88..a2a270c 100644 --- a/services/live555/docker-compose.yml +++ b/services/live555/docker-compose.yml @@ -8,7 +8,7 @@ services: ports: - "7554:554" volumes: - - ./test-data:/mnt/media + - ${MEDIA_PATH}/input/video/live555:/mnt/data networks: core_sighthound: aliases: diff --git a/services/mcp/conf/default.env b/services/mcp/conf/default.env index bb24197..7438d6b 100644 --- a/services/mcp/conf/default.env +++ b/services/mcp/conf/default.env @@ -1,5 +1,5 @@ MCP_DOCKER_IMAGE=us-central1-docker.pkg.dev/ext-edge-analytics/docker/mcp -MCP_DOCKER_TAG=1.2.9 +MCP_DOCKER_TAG=1.3.2 MCP_CONTAINER_NAME=mcp SERVICE_GROUP=-dev SIO_LICENSE_PATH=/data/sighthound/license/sighthound-license.json diff --git a/services/mcp/conf/mcp.yml b/services/mcp/conf/mcp.yml index 8792ff7..e79d7b3 100644 --- a/services/mcp/conf/mcp.yml +++ b/services/mcp/conf/mcp.yml @@ -7,8 +7,6 @@ amqp: backend: host: 0.0.0.0 port: 9097 - username: root - password: root sqlite: enable: true path: /data/sighthound/db/sqlite.db diff --git a/services/sio/conf/default.env b/services/sio/conf/default.env index 017303b..e5b8b9f 100644 --- a/services/sio/conf/default.env +++ b/services/sio/conf/default.env @@ -1,10 +1,8 @@ SIO_DOCKER_IMAGE=us-central1-docker.pkg.dev/ext-edge-analytics/docker/sio -SIO_DOCKER_TAG=r230908 +SIO_DOCKER_TAG=r231120 SIO_LOG_LEVEL=info SIO_USER_PLUGINS_DIR=/lib/sio/plugins/ SIO_APP=runPipelineSet SIO_ENTRYPOINT=/etc/sio/sio.json SIO_CONTAINER_NAME=sio SERVICE_GROUP=-dev -# Enable if nvmpi optimizations are required and supported -# SIO_ENABLE_NVMPI=1