mirror of
https://github.com/koush/scrypted.git
synced 2026-02-08 00:12:13 +00:00
Compare commits
106 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b784399afa | ||
|
|
0f16568edb | ||
|
|
7ecee115a6 | ||
|
|
34eb2be551 | ||
|
|
27ff0c8c80 | ||
|
|
51c5df6802 | ||
|
|
328bd78771 | ||
|
|
3d2ae6384f | ||
|
|
e1ba16f708 | ||
|
|
6f47e39bf3 | ||
|
|
e38c3c975f | ||
|
|
9c75b074b5 | ||
|
|
299d926eae | ||
|
|
22d0ce4f82 | ||
|
|
53c2b7cb58 | ||
|
|
86548f6fa4 | ||
|
|
0e1e641f8f | ||
|
|
58e0a748c4 | ||
|
|
b4a58df53a | ||
|
|
b83b7ff559 | ||
|
|
de2173567e | ||
|
|
9c931b21dc | ||
|
|
5291afad6a | ||
|
|
e1ac1ace87 | ||
|
|
1f6f1a82aa | ||
|
|
70af66a875 | ||
|
|
b7bab5b2e2 | ||
|
|
5d5686a9e7 | ||
|
|
1eb5012e9b | ||
|
|
3574e72e4f | ||
|
|
b7ff4dfd5e | ||
|
|
e0ed953963 | ||
|
|
930690a4ba | ||
|
|
1aa4d45caa | ||
|
|
28fb2b0853 | ||
|
|
4fae4fba3b | ||
|
|
b72c8f59eb | ||
|
|
369ad59324 | ||
|
|
51ac5a1042 | ||
|
|
200c107e97 | ||
|
|
35139abe30 | ||
|
|
dc7f305687 | ||
|
|
2a479dd38a | ||
|
|
d32f9bb07a | ||
|
|
a33bed0b44 | ||
|
|
f9847f6f72 | ||
|
|
add53d07f3 | ||
|
|
db21159299 | ||
|
|
6fa7f06852 | ||
|
|
58387e5046 | ||
|
|
1589908698 | ||
|
|
d0183c29a8 | ||
|
|
99dcdd12cf | ||
|
|
b1861e4630 | ||
|
|
193bfce979 | ||
|
|
5b7cc826a6 | ||
|
|
8484d75e82 | ||
|
|
e8fef925bb | ||
|
|
fa200e1bbf | ||
|
|
df0991b882 | ||
|
|
93ff686000 | ||
|
|
6ae9a5618d | ||
|
|
c882b9a04e | ||
|
|
af4269be49 | ||
|
|
61ad99a3f6 | ||
|
|
d71bbf1824 | ||
|
|
74674dab00 | ||
|
|
247f860a23 | ||
|
|
a801fe1f4e | ||
|
|
6744851256 | ||
|
|
10569731aa | ||
|
|
4965b1f99a | ||
|
|
510250c60b | ||
|
|
8e33775b0e | ||
|
|
1077bd1f56 | ||
|
|
a485d8ae69 | ||
|
|
17f42762e7 | ||
|
|
49943a5408 | ||
|
|
585c638220 | ||
|
|
6767892c63 | ||
|
|
289555c03e | ||
|
|
a563e17c56 | ||
|
|
54c317b217 | ||
|
|
0df9c31480 | ||
|
|
19c8436256 | ||
|
|
b73526674a | ||
|
|
fd863f4ba3 | ||
|
|
634b65c216 | ||
|
|
548086403b | ||
|
|
867432cd82 | ||
|
|
b3cc914772 | ||
|
|
b297a4d3d6 | ||
|
|
8144588bcf | ||
|
|
f3265f5fb6 | ||
|
|
f686812f01 | ||
|
|
552787e06b | ||
|
|
3c4de5af39 | ||
|
|
e08df29373 | ||
|
|
1efb624681 | ||
|
|
09afc6c96c | ||
|
|
666d2903e4 | ||
|
|
24eb60bce1 | ||
|
|
d1951687be | ||
|
|
3c3c2c1610 | ||
|
|
0f9106c639 | ||
|
|
ea628a7130 |
11
.github/workflows/docker-common.yml
vendored
11
.github/workflows/docker-common.yml
vendored
@@ -77,13 +77,14 @@ jobs:
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
build-nvidia:
|
||||
name: Push NVIDIA Docker image to Docker Hub
|
||||
build-vendor:
|
||||
name: Push Vendor Docker image to Docker Hub
|
||||
needs: build
|
||||
runs-on: self-hosted
|
||||
strategy:
|
||||
matrix:
|
||||
BASE: ["noble"]
|
||||
VENDOR: ["nvidia", "intel"]
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3
|
||||
@@ -138,11 +139,11 @@ jobs:
|
||||
build-args: |
|
||||
BASE=ghcr.io/koush/scrypted-common:${{ matrix.BASE }}-full
|
||||
context: install/docker/
|
||||
file: install/docker/Dockerfile.nvidia
|
||||
file: install/docker/Dockerfile.${{ matrix.VENDOR }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
koush/scrypted-common:${{ matrix.BASE }}-nvidia
|
||||
ghcr.io/koush/scrypted-common:${{ matrix.BASE }}-nvidia
|
||||
koush/scrypted-common:${{ matrix.BASE }}-${{ matrix.VENDOR }}
|
||||
ghcr.io/koush/scrypted-common:${{ matrix.BASE }}-${{ matrix.VENDOR }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
33
.github/workflows/docker.yml
vendored
33
.github/workflows/docker.yml
vendored
@@ -20,10 +20,11 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
BASE: [
|
||||
["noble-nvidia", ".s6", "noble-nvidia"],
|
||||
["noble-full", ".s6", "noble-full"],
|
||||
["noble-lite", "", "noble-lite"],
|
||||
# ["noble-lite", ".router", "noble-router"],
|
||||
["noble-nvidia", ".s6", "noble-nvidia", "nvidia"],
|
||||
["noble-intel", ".s6", "noble-intel", "intel"],
|
||||
["noble-full", ".s6", "noble-full", "full"],
|
||||
["noble-lite", "", "noble-lite", "lite"],
|
||||
["noble-lite", ".router", "noble-router", "router"],
|
||||
]
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
@@ -94,19 +95,25 @@ jobs:
|
||||
file: install/docker/Dockerfile${{ matrix.BASE[1] }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
# when publishing a tag (beta or latest), platform and version, create some tags as follows.
|
||||
# using beta 0.0.1 as an example
|
||||
# koush/scrypted:v0.0.1-noble-full
|
||||
# koush/scrypted:beta
|
||||
# koush/scrypted:beta-nvidia|intel|full|router|lite
|
||||
|
||||
# using latest 0.0.2 as an example:
|
||||
# koush/scrypted:v0.0.2-noble-full
|
||||
# koush/scrypted:latest
|
||||
# koush/scrypted:nvidia|intel|full|router|lite
|
||||
tags: |
|
||||
${{ format('koush/scrypted:v{1}-{0}', matrix.BASE[2], github.event.inputs.publish_tag || steps.package-version.outputs.NPM_VERSION) }}
|
||||
${{ format('koush/scrypted:v{0}-{1}', github.event.inputs.publish_tag || steps.package-version.outputs.NPM_VERSION, matrix.BASE[2]) }}
|
||||
${{ matrix.BASE[2] == 'noble-full' && format('koush/scrypted:{0}', github.event.inputs.tag) || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[2] == 'noble-nvidia' && 'koush/scrypted:nvidia' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[2] == 'noble-full' && 'koush/scrypted:full' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[2] == 'noble-lite' && matrix.BASE[1] == '' && 'koush/scrypted:lite' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[2] == 'noble-router' && 'koush/scrypted:router' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && format('koush/scrypted:{0}', matrix.BASE[3]) || '' }}
|
||||
${{ github.event.inputs.tag != 'latest' && format('koush/scrypted:{0}-{1}', github.event.inputs.tag, matrix.BASE[3]) }}
|
||||
|
||||
${{ format('ghcr.io/koush/scrypted:v{1}-{0}', matrix.BASE[0], github.event.inputs.publish_tag || steps.package-version.outputs.NPM_VERSION) }}
|
||||
${{ matrix.BASE[2] == 'noble-full' && format('ghcr.io/koush/scrypted:{0}', github.event.inputs.tag) || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[2] == 'noble-nvidia' && 'ghcr.io/koush/scrypted:nvidia' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[2] == 'noble-full' && 'ghcr.io/koush/scrypted:full' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[2] == 'noble-lite' && matrix.BASE[1] == '' && 'ghcr.io/koush/scrypted:lite' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[2] == 'noble-lite' && 'ghcr.io/koush/scrypted:router' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && format('ghcr.io/koush/scrypted:{0}', matrix.BASE[3]) || ''}}
|
||||
${{ github.event.inputs.tag != 'latest' && format('ghcr.io/koush/scrypted:{0}-{1}', github.event.inputs.tag, matrix.BASE[3]) || '' }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
58
common/package-lock.json
generated
58
common/package-lock.json
generated
@@ -10,6 +10,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/types": "^0.5.27",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
@@ -21,28 +22,29 @@
|
||||
},
|
||||
"../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.5.3",
|
||||
"version": "0.5.29",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@rollup/plugin-commonjs": "^28.0.5",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"axios": "^1.10.0",
|
||||
"babel-loader": "^10.0.0",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"openai": "^5.3.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.27.4",
|
||||
"rollup": "^4.43.0",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-loader": "^9.5.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"typescript": "^5.8.3",
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
},
|
||||
"bin": {
|
||||
@@ -55,9 +57,9 @@
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/node": "^24.0.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.11"
|
||||
"typedoc": "^0.28.5"
|
||||
}
|
||||
},
|
||||
"../sdk/node_modules/@ampproject/remapping": {
|
||||
@@ -3308,6 +3310,15 @@
|
||||
"resolved": "../sdk",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@scrypted/types": {
|
||||
"version": "0.5.27",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.5.27.tgz",
|
||||
"integrity": "sha512-1SAEa6Js1VeAzGtaCQXXpNc2Ty1ZB6aqqNLtsoPeeuNw+JlSdK42sX4wVnzKxkAOcS1WZiC1fj6DV9B/CNyGtA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"openai": "^5.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.9",
|
||||
"dev": true,
|
||||
@@ -3393,6 +3404,27 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/openai": {
|
||||
"version": "5.8.2",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-5.8.2.tgz",
|
||||
"integrity": "sha512-8C+nzoHYgyYOXhHGN6r0fcb4SznuEn1R7YZMvlqDbnCuE0FM2mm3T1HiYW6WIcMS/F1Of2up/cSPjLPaWt0X9Q==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"openai": "bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ws": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.9.2",
|
||||
"dev": true,
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/types": "^0.5.27",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
|
||||
5
common/src/devices.ts
Normal file
5
common/src/devices.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { SystemManager } from '@scrypted/types';
|
||||
|
||||
export function getAllDevices<T>(systemManager: SystemManager) {
|
||||
return Object.keys(systemManager.getSystemState()).map(id => systemManager.getDeviceById<T>(id));
|
||||
}
|
||||
8
common/src/json.ts
Normal file
8
common/src/json.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
export function safeParseJson(value: string) {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
}
|
||||
2
external/ring-client-api
vendored
2
external/ring-client-api
vendored
Submodule external/ring-client-api updated: d9f51b8b6d...516e96a24e
@@ -1,6 +1,6 @@
|
||||
# Home Assistant Addon Configuration
|
||||
name: Scrypted
|
||||
version: "v0.130.1-noble-full"
|
||||
version: "v0.139.0-noble-full"
|
||||
slug: scrypted
|
||||
description: Scrypted is a high performance home video integration and automation platform
|
||||
url: "https://github.com/koush/scrypted"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
ARG BASE="20-jammy-full"
|
||||
ARG BASE="noble-full"
|
||||
FROM ghcr.io/koush/scrypted-common:${BASE}
|
||||
|
||||
WORKDIR /
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
# install script.
|
||||
################################################################
|
||||
ARG BASE="noble"
|
||||
FROM ubuntu:${BASE} as header
|
||||
FROM ubuntu:${BASE} AS header
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
@@ -61,7 +61,7 @@ RUN python3 -m pip install debugpy
|
||||
################################################################
|
||||
# Begin section generated from template/Dockerfile.full.footer
|
||||
################################################################
|
||||
FROM header as base
|
||||
FROM header AS base
|
||||
|
||||
# vulkan
|
||||
RUN apt -y install libvulkan1
|
||||
|
||||
9
install/docker/Dockerfile.intel
Normal file
9
install/docker/Dockerfile.intel
Normal file
@@ -0,0 +1,9 @@
|
||||
ARG BASE="ghcr.io/koush/scrypted-common:20-jammy-full"
|
||||
FROM $BASE
|
||||
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="intel"
|
||||
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-oneapi.sh | bash
|
||||
# these paths must be updated if oneapi is updated via the install-intel-oneapi.sh script
|
||||
# note that the 2022.2 seems to be a typo in the intel script...?
|
||||
ENV LD_LIBRARY_PATH=/opt/intel/oneapi/tcm/1.4/lib:/opt/intel/oneapi/umf/0.11/lib:/opt/intel/oneapi/tbb/2022.2/env/../lib/intel64/gcc4.8:/opt/intel/oneapi/mkl/2025.2/lib:/opt/intel/oneapi/compiler/2025.2/opt/compiler/lib:/opt/intel/oneapi/compiler/2025.2/lib
|
||||
@@ -1,5 +1,7 @@
|
||||
ARG BASE="jammy"
|
||||
FROM ubuntu:${BASE} as header
|
||||
FROM ubuntu:${BASE} AS header
|
||||
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="lite"
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
@@ -26,5 +28,3 @@ ENV SHELL="/bin/bash"
|
||||
RUN test -f "/usr/bin/python3" && test -f "/usr/bin/python3.12"
|
||||
ENV SCRYPTED_PYTHON_PATH="/usr/bin/python3"
|
||||
ENV SCRYPTED_PYTHON312_PATH="/usr/bin/python3.12"
|
||||
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="lite"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
ARG BASE="ghcr.io/koush/scrypted-common:20-jammy-full"
|
||||
FROM $BASE
|
||||
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="nvidia"
|
||||
|
||||
ENV NVIDIA_DRIVER_CAPABILITIES=all
|
||||
ENV NVIDIA_VISIBLE_DEVICES=all
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
ARG BASE="noble-lite"
|
||||
FROM ghcr.io/koush/scrypted-common:${BASE}
|
||||
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="router"
|
||||
|
||||
# tools
|
||||
RUN apt -y update && apt -y install nano net-tools dnsutils dnsmasq vlan bridge-utils netplan.io nftables isc-dhcp-client
|
||||
RUN apt -y update && apt -y install nano net-tools dnsutils dnsmasq vlan bridge-utils netplan.io nftables isc-dhcp-client cron
|
||||
RUN rm -f /etc/systemd/system/multi-user.target.wants/dnsmasq.service
|
||||
RUN rm -f /etc/systemd/system/sysinit.target.wants/systemd-resolved.service
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ RUN apt-get update && apt-get -y install \
|
||||
libavahi-compat-libdnssd-dev \
|
||||
xz-utils
|
||||
|
||||
# killall
|
||||
RUN apt -y install psmisc
|
||||
|
||||
# copy configurations and scripts
|
||||
COPY fs /
|
||||
|
||||
|
||||
@@ -45,10 +45,14 @@ services:
|
||||
# - SCRYPTED_DOCKER_AVAHI=true
|
||||
|
||||
# NVIDIA (Part 1 of 2)
|
||||
# runtime: nvidia
|
||||
# nvidia runtime: nvidia
|
||||
|
||||
# NVIDIA (Part 2 of 2) - Use NVIDIA image, and remove subsequent default image.
|
||||
# image: ghcr.io/koush/scrypted:nvidia
|
||||
# Valid images:
|
||||
# ghcr.io/koush/scrypted
|
||||
# ghcr.io/koush/scrypted:nvidia
|
||||
# ghcr.io/koush/scrypted:intel
|
||||
# ghcr.io/koush/scrypted:lite
|
||||
image: ghcr.io/koush/scrypted
|
||||
|
||||
volumes:
|
||||
|
||||
@@ -72,12 +72,12 @@ apt-get install -y ocl-icd-libopencl1
|
||||
# https://github.com/intel/compute-runtime/releases/tag/24.35.30872.22
|
||||
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.20/intel-igc-core_1.0.17537.20_amd64.deb
|
||||
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.20/intel-igc-opencl_1.0.17537.20_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-level-zero-gpu-dbgsym_1.3.30872.22_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-level-zero-gpu-legacy1-dbgsym_1.3.30872.22_amd64.ddeb
|
||||
# curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-level-zero-gpu-dbgsym_1.3.30872.22_amd64.ddeb
|
||||
# curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-level-zero-gpu-legacy1-dbgsym_1.3.30872.22_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-level-zero-gpu-legacy1_1.3.30872.22_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-level-zero-gpu_1.3.30872.22_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-opencl-icd-dbgsym_24.35.30872.22_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-opencl-icd-legacy1-dbgsym_24.35.30872.22_amd64.ddeb
|
||||
# curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-opencl-icd-dbgsym_24.35.30872.22_amd64.ddeb
|
||||
# curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-opencl-icd-legacy1-dbgsym_24.35.30872.22_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-opencl-icd-legacy1_24.35.30872.22_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-opencl-icd_24.35.30872.22_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/libigdgmm12_22.5.0_amd64.deb
|
||||
@@ -85,20 +85,17 @@ curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.3087
|
||||
dpkg -i *.deb
|
||||
rm -f *.deb
|
||||
|
||||
# https://github.com/intel/compute-runtime/releases/tag/24.45.31740.9
|
||||
# https://github.com/intel/compute-runtime/releases
|
||||
# note that at time of commit, IGC supports ubuntu 24.04 only possibly due to their builder being on 24.04.
|
||||
IGC_BASE_VERSION=2.5.6
|
||||
IGC_VERSION=2_$IGC_BASE_VERSION+18417_amd64
|
||||
COMPUTE_VERSION=24.52.32224.5
|
||||
ZERO_GPU_VERSION=1.6.32224.5_amd64
|
||||
LIBIGDGMM_VERSION=22.5.5_amd64
|
||||
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/v$IGC_BASE_VERSION/intel-igc-core-$IGC_VERSION.deb
|
||||
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/v$IGC_BASE_VERSION/intel-igc-opencl-$IGC_VERSION.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/$COMPUTE_VERSION/intel-level-zero-gpu-dbgsym_$ZERO_GPU_VERSION.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/$COMPUTE_VERSION/intel-level-zero-gpu_$ZERO_GPU_VERSION.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/$COMPUTE_VERSION/intel-opencl-icd-dbgsym_"$COMPUTE_VERSION"_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/$COMPUTE_VERSION/intel-opencl-icd_"$COMPUTE_VERSION"_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/$COMPUTE_VERSION/libigdgmm12_$LIBIGDGMM_VERSION.deb
|
||||
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/v2.12.5/intel-igc-core-2_2.12.5+19302_amd64.deb
|
||||
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/v2.12.5/intel-igc-opencl-2_2.12.5+19302_amd64.deb
|
||||
# curl -O -L https://github.com/intel/compute-runtime/releases/download/25.22.33944.8/intel-ocloc-dbgsym_25.22.33944.8-0_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/25.22.33944.8/intel-ocloc_25.22.33944.8-0_amd64.deb
|
||||
# curl -O -L https://github.com/intel/compute-runtime/releases/download/25.22.33944.8/intel-opencl-icd-dbgsym_25.22.33944.8-0_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/25.22.33944.8/intel-opencl-icd_25.22.33944.8-0_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/25.22.33944.8/libigdgmm12_22.7.0_amd64.deb
|
||||
# curl -O -L https://github.com/intel/compute-runtime/releases/download/25.22.33944.8/libze-intel-gpu1-dbgsym_25.22.33944.8-0_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/25.22.33944.8/libze-intel-gpu1_25.22.33944.8-0_amd64.deb
|
||||
|
||||
set +e
|
||||
dpkg -i *.deb
|
||||
|
||||
@@ -38,15 +38,15 @@ set -e
|
||||
rm -rf /tmp/npu && mkdir -p /tmp/npu && cd /tmp/npu
|
||||
|
||||
# level zero must also be installed
|
||||
LEVEL_ZERO_VERSION=1.19.2
|
||||
LEVEL_ZERO_VERSION=1.22.4
|
||||
# https://github.com/oneapi-src/level-zero
|
||||
curl -O -L https://github.com/oneapi-src/level-zero/releases/download/v"$LEVEL_ZERO_VERSION"/level-zero_"$LEVEL_ZERO_VERSION"+u$distro.deb
|
||||
curl -O -L https://github.com/oneapi-src/level-zero/releases/download/v"$LEVEL_ZERO_VERSION"/level-zero-devel_"$LEVEL_ZERO_VERSION"+u$distro.deb
|
||||
|
||||
# npu driver
|
||||
# https://github.com/intel/linux-npu-driver
|
||||
NPU_VERSION=1.13.0
|
||||
NPU_VERSION_DATE=20250131-13074932693
|
||||
NPU_VERSION=1.19.0
|
||||
NPU_VERSION_DATE=20250707-16111289554
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v"$NPU_VERSION"/intel-driver-compiler-npu_$NPU_VERSION."$NPU_VERSION_DATE"_ubuntu$distro.deb
|
||||
# firmware can only be installed on host. will cause problems inside container.
|
||||
if [ -n "$INTEL_FW_NPU" ]
|
||||
|
||||
18
install/docker/install-intel-oneapi.sh
Normal file
18
install/docker/install-intel-oneapi.sh
Normal file
@@ -0,0 +1,18 @@
|
||||
if [ "$(uname -m)" = "x86_64" ]
|
||||
then
|
||||
apt -y update
|
||||
apt -y install gpg
|
||||
|
||||
# download the key to system keyring
|
||||
curl -1sLf https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB | gpg --dearmor --yes --output /usr/share/keyrings/oneapi-archive-keyring.gpg
|
||||
|
||||
# add signed entry to apt sources and configure the APT client to use Intel repository:
|
||||
echo "deb [signed-by=/usr/share/keyrings/oneapi-archive-keyring.gpg] https://apt.repos.intel.com/oneapi all main" | tee /etc/apt/sources.list.d/oneAPI.list
|
||||
|
||||
apt -y update
|
||||
apt -y install intel-oneapi-mkl-sycl-blas intel-oneapi-runtime-dnnl intel-oneapi-runtime-compilers
|
||||
else
|
||||
echo "NVIDIA graphics will not be installed on this architecture."
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -36,7 +36,8 @@ curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | gpg --yes --dea
|
||||
tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
|
||||
apt -y update
|
||||
# is there a way to get a versioned package automatically?
|
||||
apt -y install cuda-drivers
|
||||
# cuda-drivers does not work with blackwell for some reason, container toolkit it broken IIRC.
|
||||
apt -y install nvidia-open
|
||||
apt -y install nvidia-container-toolkit
|
||||
|
||||
nvidia-ctk runtime configure --runtime=docker
|
||||
|
||||
@@ -23,7 +23,7 @@ then
|
||||
&& wget -qO /cuda-keyring.deb https://developer.download.nvidia.com/compute/cuda/repos/$distro/$(uname -m)/cuda-keyring_1.1-1_all.deb \
|
||||
&& dpkg -i /cuda-keyring.deb \
|
||||
&& apt update -q \
|
||||
&& apt install -y cuda-nvcc-12-6 libcublas-12-6 libcudnn9-cuda-12 cuda-libraries-12-6;
|
||||
&& apt install -y cuda-nvcc-12-9 libcublas-12-9 libcudnn9-cuda-12 cuda-libraries-12-9;
|
||||
|
||||
if [ "$?" != "0" ]
|
||||
then
|
||||
|
||||
@@ -13,6 +13,8 @@ then
|
||||
fi
|
||||
|
||||
function readyn() {
|
||||
echo
|
||||
echo
|
||||
if [ ! -z "$SCRYPTED_NONINTERACTIVE" ]
|
||||
then
|
||||
yn="y"
|
||||
@@ -51,6 +53,9 @@ rm -rf $SCRYPTED_HOME/install.json
|
||||
rm -rf $SCRYPTED_HOME/package.json
|
||||
rm -rf $SCRYPTED_HOME/package-lock.json
|
||||
|
||||
# must get this value as grep returns non zero if empty
|
||||
HAS_NVIDIA=$(lspci | grep -i nvidia)
|
||||
|
||||
set -e
|
||||
cd $SCRYPTED_HOME
|
||||
|
||||
@@ -93,6 +98,24 @@ else
|
||||
sudo apt -y purge apparmor || true
|
||||
fi
|
||||
|
||||
if [ ! -z "$HAS_NVIDIA" ]
|
||||
then
|
||||
readyn "NVIDIA GPU detected. Use NVIDIA image for GPU acceleration?"
|
||||
if [ "$yn" == "y" ]
|
||||
then
|
||||
readyn "NVIDIA image requires the NVIDIA Drivers and Container Toolkit to be installed. This script can install them for you. Install NVIDIA Drivers and Container Toolkit for GPU acceleration?"
|
||||
if [ "$yn" == "y" ]
|
||||
then
|
||||
curl -fsSL https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-nvidia-container-toolkit.sh -o install-nvidia-container-toolkit.sh
|
||||
chmod +x install-nvidia-container-toolkit.sh
|
||||
./install-nvidia-container-toolkit.sh
|
||||
rm install-nvidia-container-toolkit.sh
|
||||
fi
|
||||
sed -i 's/'#' nvidia //g' $DOCKER_COMPOSE_YML
|
||||
sed -i 's/ghcr.io\/koush\/scrypted/ghcr.io\/koush\/scrypted:nvidia/g' $DOCKER_COMPOSE_YML
|
||||
fi
|
||||
fi
|
||||
|
||||
readyn "Install avahi-daemon? This is the recommended for reliable HomeKit discovery and pairing."
|
||||
if [ "$yn" == "y" ]
|
||||
then
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
################################################################
|
||||
# Begin section generated from template/Dockerfile.full.footer
|
||||
################################################################
|
||||
FROM header as base
|
||||
FROM header AS base
|
||||
|
||||
# vulkan
|
||||
RUN apt -y install libvulkan1
|
||||
|
||||
@@ -18,7 +18,7 @@ function readyn() {
|
||||
}
|
||||
|
||||
cd /tmp
|
||||
SCRYPTED_VERSION=v0.137.0
|
||||
SCRYPTED_VERSION=v0.139.0
|
||||
SCRYPTED_TAR_ZST=scrypted-$SCRYPTED_VERSION.tar.zst
|
||||
if [ -z "$VMID" ]
|
||||
then
|
||||
|
||||
@@ -27,7 +27,8 @@ async function getAuth(options: AuthFetchOptions, url: string | URL, method: str
|
||||
++credential.count;
|
||||
const nc = ('00000000' + credential.count).slice(-8);
|
||||
const cnonce = [...Array(24)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');
|
||||
const uri = new URL(url).pathname;
|
||||
const parsedURL = new URL(url);
|
||||
const uri = parsedURL.pathname + parsedURL.search;
|
||||
|
||||
const { DIGEST, buildAuthorizationHeader } = await import('http-auth-utils');
|
||||
|
||||
|
||||
94
packages/client/package-lock.json
generated
94
packages/client/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.17",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.17",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"engine.io-client": "^6.6.3",
|
||||
@@ -15,13 +15,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ip": "^1.1.3",
|
||||
"@types/node": "^22.13.10",
|
||||
"@types/ws": "^8.18.0",
|
||||
"@types/node": "^24.0.10",
|
||||
"@types/ws": "^8.18.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.8.2"
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@scrypted/types": "^0.5.12"
|
||||
"@scrypted/types": "^0.5.23"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
@@ -83,11 +83,59 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/types": {
|
||||
"version": "0.5.12",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.5.12.tgz",
|
||||
"integrity": "sha512-nTwcMHZyH3nXThL22eNcVw7OjSyL5qoTgUay6K7y43HKz1mBnFEmIUkW8eLdyP4nbpwwA0b60MOPDKZVnssB0Q==",
|
||||
"version": "0.5.23",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.5.23.tgz",
|
||||
"integrity": "sha512-is/UJHgS3lvEuXyb+C/OPeIP5CKp+M6SQt1l/WFJr1Oj+KYYHGU8Ztlh/qOmAWgONhg286N4/cLNzTtAAh4YnA==",
|
||||
"license": "ISC",
|
||||
"peer": true
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"openai": "^5.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/types/node_modules/openai": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-5.3.0.tgz",
|
||||
"integrity": "sha512-VIKmoF7y4oJCDOwP/oHXGzM69+x0dpGFmN9QmYO+uPbLFOmmnwO+x1GbsgUtI+6oraxomGZ566Y421oYVu191w==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"openai": "bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ws": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/types/node_modules/ws": {
|
||||
"version": "8.18.2",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
|
||||
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.2",
|
||||
@@ -134,19 +182,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.13.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz",
|
||||
"integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==",
|
||||
"version": "24.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz",
|
||||
"integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
"undici-types": "~7.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.0.tgz",
|
||||
"integrity": "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==",
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -684,9 +732,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
|
||||
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -698,9 +746,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
|
||||
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.17",
|
||||
"description": "",
|
||||
"main": "dist/packages/client/src/index.js",
|
||||
"scripts": {
|
||||
@@ -13,13 +13,13 @@
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/ip": "^1.1.3",
|
||||
"@types/node": "^22.13.10",
|
||||
"@types/ws": "^8.18.0",
|
||||
"@types/node": "^24.0.10",
|
||||
"@types/ws": "^8.18.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.8.2"
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@scrypted/types": "^0.5.12"
|
||||
"@scrypted/types": "^0.5.23"
|
||||
},
|
||||
"dependencies": {
|
||||
"engine.io-client": "^6.6.3",
|
||||
|
||||
947
packages/deferred/package-lock.json
generated
947
packages/deferred/package-lock.json
generated
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "@scrypted/rpc",
|
||||
"version": "0.0.5",
|
||||
"name": "@scrypted/deferred",
|
||||
"version": "0.0.8",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/rpc",
|
||||
"version": "0.0.5",
|
||||
"name": "@scrypted/deferred",
|
||||
"version": "0.0.8",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"rimraf": "^4.1.1",
|
||||
"typescript": "^4.7.4"
|
||||
"@types/node": "^24.0.10",
|
||||
"rimraf": "^6.0.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
},
|
||||
"../../common": {
|
||||
@@ -43,19 +43,141 @@
|
||||
"../sdk/types": {
|
||||
"extraneous": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.11.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
||||
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-4.1.1.tgz",
|
||||
"integrity": "sha512-Z4Y81w8atcvaJuJuBB88VpADRH66okZAuEm+Jtaufa+s7rZmIz+Hik2G53kGaNytE7lsfXyWktTmfVz0H9xuDg==",
|
||||
"node_modules/@isaacs/balanced-match": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"rimraf": "dist/cjs/src/bin.js"
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/brace-expansion": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
|
||||
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@isaacs/balanced-match": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^5.1.2",
|
||||
"string-width-cjs": "npm:string-width@^4.2.0",
|
||||
"strip-ansi": "^7.0.1",
|
||||
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
|
||||
"wrap-ansi": "^8.1.0",
|
||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz",
|
||||
"integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
||||
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.6",
|
||||
"signal-exit": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
@@ -64,38 +186,793 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.7.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
|
||||
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
|
||||
"node_modules/glob": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
|
||||
"integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.3.1",
|
||||
"jackspeak": "^4.1.1",
|
||||
"minimatch": "^10.0.3",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jackspeak": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz",
|
||||
"integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz",
|
||||
"integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
|
||||
"integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@isaacs/brace-expansion": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
|
||||
"integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"lru-cache": "^11.0.0",
|
||||
"minipass": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz",
|
||||
"integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"glob": "^11.0.0",
|
||||
"package-json-from-dist": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"rimraf": "dist/esm/bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eastasianwidth": "^0.2.0",
|
||||
"emoji-regex": "^9.2.2",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs": {
|
||||
"name": "string-width",
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs": {
|
||||
"name": "strip-ansi",
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
|
||||
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.1.0",
|
||||
"string-width": "^5.0.1",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs": {
|
||||
"name": "wrap-ansi",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": {
|
||||
"version": "18.11.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
||||
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
|
||||
"@isaacs/balanced-match": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@isaacs/brace-expansion": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
|
||||
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@isaacs/balanced-match": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"string-width": "^5.1.2",
|
||||
"string-width-cjs": "npm:string-width@^4.2.0",
|
||||
"strip-ansi": "^7.0.1",
|
||||
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
|
||||
"wrap-ansi": "^8.1.0",
|
||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "24.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz",
|
||||
"integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"undici-types": "~7.8.0"
|
||||
}
|
||||
},
|
||||
"ansi-regex": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
|
||||
"dev": true
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
||||
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
||||
"dev": true
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"dev": true
|
||||
},
|
||||
"emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"dev": true
|
||||
},
|
||||
"foreground-child": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cross-spawn": "^7.0.6",
|
||||
"signal-exit": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"glob": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
|
||||
"integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"foreground-child": "^3.3.1",
|
||||
"jackspeak": "^4.1.1",
|
||||
"minimatch": "^10.0.3",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true
|
||||
},
|
||||
"isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true
|
||||
},
|
||||
"jackspeak": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz",
|
||||
"integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
}
|
||||
},
|
||||
"lru-cache": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz",
|
||||
"integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==",
|
||||
"dev": true
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
|
||||
"integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@isaacs/brace-expansion": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"minipass": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"dev": true
|
||||
},
|
||||
"package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||
"dev": true
|
||||
},
|
||||
"path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true
|
||||
},
|
||||
"path-scurry": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
|
||||
"integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lru-cache": "^11.0.0",
|
||||
"minipass": "^7.1.2"
|
||||
}
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-4.1.1.tgz",
|
||||
"integrity": "sha512-Z4Y81w8atcvaJuJuBB88VpADRH66okZAuEm+Jtaufa+s7rZmIz+Hik2G53kGaNytE7lsfXyWktTmfVz0H9xuDg==",
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz",
|
||||
"integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"glob": "^11.0.0",
|
||||
"package-json-from-dist": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.7.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
|
||||
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
|
||||
"signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"dev": true
|
||||
},
|
||||
"string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"eastasianwidth": "^0.2.0",
|
||||
"emoji-regex": "^9.2.2",
|
||||
"strip-ansi": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"string-width-cjs": {
|
||||
"version": "npm:string-width@4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true
|
||||
},
|
||||
"emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
}
|
||||
},
|
||||
"strip-ansi-cjs": {
|
||||
"version": "npm:strip-ansi@6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true
|
||||
},
|
||||
"undici-types": {
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
|
||||
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
|
||||
"dev": true
|
||||
},
|
||||
"which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"isexe": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"wrap-ansi": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^6.1.0",
|
||||
"string-width": "^5.0.1",
|
||||
"strip-ansi": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"wrap-ansi-cjs": {
|
||||
"version": "npm:wrap-ansi@7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true
|
||||
},
|
||||
"string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/deferred",
|
||||
"version": "0.0.5",
|
||||
"version": "0.0.8",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
@@ -12,8 +12,8 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"rimraf": "^4.1.1",
|
||||
"typescript": "^4.7.4"
|
||||
"@types/node": "^24.0.10",
|
||||
"rimraf": "^6.0.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
4
plugins/amcrest/package-lock.json
generated
4
plugins/amcrest/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.165",
|
||||
"version": "0.0.166",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.165",
|
||||
"version": "0.0.166",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.165",
|
||||
"version": "0.0.166",
|
||||
"description": "Amcrest Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { AuthFetchCredentialState, HttpFetchOptions, authHttpFetch } from '@scrypted/common/src/http-auth-fetch';
|
||||
import { AuthFetchCredentialState, authHttpFetch, HttpFetchOptions } from '@scrypted/common/src/http-auth-fetch';
|
||||
import { readLine } from '@scrypted/common/src/read-stream';
|
||||
import { parseHeaders, readBody } from '@scrypted/common/src/rtsp-server';
|
||||
import { MediaStreamConfiguration, Point } from '@scrypted/sdk';
|
||||
import contentType from 'content-type';
|
||||
import { IncomingMessage } from 'http';
|
||||
import { EventEmitter, Readable } from 'stream';
|
||||
import { createRtspMediaStreamOptions, Destroyable, UrlMediaStreamOptions } from '../../rtsp/src/rtsp';
|
||||
import { getDeviceInfo } from './probe';
|
||||
import { MediaStreamConfiguration, MediaStreamOptions, Point } from '@scrypted/sdk';
|
||||
|
||||
export interface AmcrestObjectDetails {
|
||||
Action: string;
|
||||
@@ -81,8 +81,11 @@ async function readAmcrestMessage(client: Readable): Promise<string[]> {
|
||||
}
|
||||
}
|
||||
|
||||
function findValue(blob: string, prefix: string, key: string) {
|
||||
const lines = blob.split('\n');
|
||||
function getLines(blob: string) {
|
||||
return blob.split(/\r?\n/).filter(line => line);
|
||||
}
|
||||
|
||||
function findValue(lines: string[], prefix: string, key: string) {
|
||||
const value = lines.find(line => line.startsWith(`${prefix}.${key}`));
|
||||
if (!value)
|
||||
return;
|
||||
@@ -124,7 +127,7 @@ const amcrestResolutions = {
|
||||
"720P": [1280, 720],
|
||||
"D1": [704, 480],
|
||||
"HD1": [352, 480],
|
||||
"BCIF": [704, 240],
|
||||
"BCIF": [528, 240],
|
||||
"2CIF": [704, 240],
|
||||
"CIF": [352, 240],
|
||||
"QCIF": [176, 120],
|
||||
@@ -133,7 +136,21 @@ const amcrestResolutions = {
|
||||
"QVGA": [320, 240]
|
||||
};
|
||||
|
||||
function fromAmcrestResolution(resolution: string) {
|
||||
const palAmcrestResolutions = {
|
||||
"D1": [704, 576],
|
||||
"HD1": [352, 576],
|
||||
"BCIF": [528, 288],
|
||||
"2CIF": [704, 288],
|
||||
"CIF": [352, 288],
|
||||
"QCIF": [176, 144],
|
||||
};
|
||||
|
||||
function fromAmcrestResolution(resolution: string, videoStandard: string) {
|
||||
if (videoStandard === 'PAL') {
|
||||
const named = palAmcrestResolutions[resolution];
|
||||
if (named)
|
||||
return named;
|
||||
}
|
||||
const named = amcrestResolutions[resolution];
|
||||
if (named)
|
||||
return named;
|
||||
@@ -438,6 +455,12 @@ export class AmcrestCameraClient {
|
||||
|
||||
this.console.log(capsResponse.body);
|
||||
|
||||
const videoStandardResponse = await this.request({
|
||||
url: `http://${this.ip}/cgi-bin/configManager.cgi?action=getConfig&name=VideoStandard`,
|
||||
responseType: 'text',
|
||||
});
|
||||
this.console.log(videoStandardResponse.body);
|
||||
|
||||
const formatNumber = Math.max(0, parseInt(options.id?.substring('channel'.length)) - 1);
|
||||
const format = options.id === 'channel0' ? 'MainFormat' : 'ExtraFormat';
|
||||
const encode = `Encode[${cameraNumber - 1}].${format}[${formatNumber}]`;
|
||||
@@ -493,17 +516,19 @@ export class AmcrestCameraClient {
|
||||
|
||||
const caps = `caps[${cameraNumber - 1}].${format}[${formatNumber}]`;
|
||||
const singleCaps = `caps.${format}[${formatNumber}]`;
|
||||
const capsLines = getLines(capsResponse.body);
|
||||
const videoStandard = findValue(getLines(videoStandardResponse.body), 'table', 'VideoStandard');
|
||||
|
||||
const findCaps = (key: string) => {
|
||||
const found = findValue(capsResponse.body, caps, key);
|
||||
const found = findValue(capsLines, caps, key);
|
||||
if (found)
|
||||
return found;
|
||||
// ad410 doesnt return a camera number if accessed directly
|
||||
if (cameraNumber - 1 === 0)
|
||||
return findValue(capsResponse.body, singleCaps, key);
|
||||
return findValue(capsLines, singleCaps, key);
|
||||
}
|
||||
|
||||
const resolutions = findCaps('Video.ResolutionTypes').split(',').map(fromAmcrestResolution);
|
||||
const resolutions = findCaps('Video.ResolutionTypes').split(',').map(r => fromAmcrestResolution(r, videoStandard));
|
||||
const bitrates = findCaps('Video.BitRateOptions').split(',').map(s => parseInt(s) * 1000);
|
||||
const fpsMax = parseInt(findCaps('Video.FPSMax'));
|
||||
const vso: MediaStreamConfiguration = {
|
||||
@@ -533,6 +558,7 @@ export class AmcrestCameraClient {
|
||||
responseType: 'text',
|
||||
});
|
||||
this.console.log(encodeResponse.body);
|
||||
const encodeLines = getLines(encodeResponse.body);
|
||||
|
||||
for (let i = 0; i < vsos.length; i++) {
|
||||
const vso = vsos[i];
|
||||
@@ -544,27 +570,27 @@ export class AmcrestCameraClient {
|
||||
encName = `table.Encode[${cameraNumber - 1}].ExtraFormat[${i - 1}]`;
|
||||
}
|
||||
|
||||
const videoCodec = fromAmcrestVideoCodec(findValue(encodeResponse.body, encName, 'Video.Compression'));
|
||||
const audioCodec = fromAmcrestAudioCodec(findValue(encodeResponse.body, encName, 'Audio.Compression'));
|
||||
const videoCodec = fromAmcrestVideoCodec(findValue(encodeLines, encName, 'Video.Compression'));
|
||||
const audioCodec = fromAmcrestAudioCodec(findValue(encodeLines, encName, 'Audio.Compression'));
|
||||
|
||||
if (vso.audio)
|
||||
vso.audio.codec = audioCodec;
|
||||
vso.video.codec = videoCodec;
|
||||
|
||||
const width = findValue(encodeResponse.body, encName, 'Video.Width');
|
||||
const height = findValue(encodeResponse.body, encName, 'Video.Height');
|
||||
const width = findValue(encodeLines, encName, 'Video.Width');
|
||||
const height = findValue(encodeLines, encName, 'Video.Height');
|
||||
if (width && height) {
|
||||
vso.video.width = parseInt(width);
|
||||
vso.video.height = parseInt(height);
|
||||
}
|
||||
|
||||
const videoEnable = findValue(encodeResponse.body, encName, 'VideoEnable');
|
||||
const videoEnable = findValue(encodeLines, encName, 'VideoEnable');
|
||||
if (videoEnable?.trim() === 'false') {
|
||||
this.console.warn('Video stream is disabled and should likely be enabled:', encName);
|
||||
continue;
|
||||
}
|
||||
|
||||
const encodeOptions = findValue(encodeResponse.body, encName, 'Video.BitRate');
|
||||
const encodeOptions = findValue(encodeLines, encName, 'Video.BitRate');
|
||||
if (!encodeOptions)
|
||||
continue;
|
||||
|
||||
|
||||
4
plugins/core/package-lock.json
generated
4
plugins/core/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.3.120",
|
||||
"version": "0.3.129",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.3.120",
|
||||
"version": "0.3.129",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.3.120",
|
||||
"version": "0.3.129",
|
||||
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DeviceState, MixinProvider, Readme, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
|
||||
import { typeToIcon } from "../../../../manage.scrypted.app/src/device-icons";
|
||||
import { typeToIcon } from "../../../../manage.scrypted.app/src/util/device-icons";
|
||||
|
||||
export class LauncherMixin extends ScryptedDeviceBase implements MixinProvider, Readme {
|
||||
async getReadmeMarkdown(): Promise<string> {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { readFileAsString, tsCompile } from '@scrypted/common/src/eval/scrypted-eval';
|
||||
import { sleep } from '@scrypted/common/src/sleep';
|
||||
import sdk, { DeviceProvider, HttpRequest, HttpRequestHandler, HttpResponse, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, SettingValue, Settings } from '@scrypted/sdk';
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import { writeFileSync } from 'fs';
|
||||
@@ -8,6 +9,7 @@ import yaml from 'yaml';
|
||||
import { getUsableNetworkAddresses } from '../../../server/src/ip';
|
||||
import { AggregateCore, AggregateCoreNativeId } from './aggregate-core';
|
||||
import { AutomationCore, AutomationCoreNativeId } from './automations-core';
|
||||
import { ClusterCore, ClusterCoreNativeId } from './cluster';
|
||||
import { LauncherMixin } from './launcher-mixin';
|
||||
import { MediaCore } from './media-core';
|
||||
import { checkLegacyLxc, checkLxc } from './platform/lxc';
|
||||
@@ -15,7 +17,6 @@ import { ConsoleServiceNativeId, PluginSocketService, ReplServiceNativeId } from
|
||||
import { ScriptCore, ScriptCoreNativeId, newScript } from './script-core';
|
||||
import { TerminalService, TerminalServiceNativeId, newTerminalService } from './terminal-service';
|
||||
import { UsersCore, UsersNativeId } from './user';
|
||||
import { ClusterCore, ClusterCoreNativeId } from './cluster';
|
||||
|
||||
const { deviceManager, endpointManager } = sdk;
|
||||
|
||||
@@ -210,6 +211,32 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Dev
|
||||
},
|
||||
);
|
||||
})();
|
||||
|
||||
// check on workers once an hour.
|
||||
this.updateWorkers();
|
||||
setInterval(() => this.updateWorkers(), 1000 * 60 * 60);
|
||||
}
|
||||
|
||||
async updateWorkers() {
|
||||
const workers = await sdk.clusterManager?.getClusterWorkers();
|
||||
if (!workers)
|
||||
return;
|
||||
for (const [id, worker] of Object.entries(workers)) {
|
||||
const forked = sdk.fork<ReturnType<typeof fork>>({
|
||||
clusterWorkerId: id,
|
||||
runtime: 'node',
|
||||
});
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const result = await forked.result;
|
||||
result.checkLxc();
|
||||
}
|
||||
catch (e) {
|
||||
forked.worker.terminate();
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
async getSettings(): Promise<Setting[]> {
|
||||
@@ -332,5 +359,15 @@ export async function fork() {
|
||||
tsCompile,
|
||||
newScript,
|
||||
newTerminalService,
|
||||
checkLxc: async () => {
|
||||
try {
|
||||
// console.warn('Checking for LXC installation...');
|
||||
await checkLxc();
|
||||
}
|
||||
finally {
|
||||
await sleep(1000);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ export async function checkLxc() {
|
||||
return;
|
||||
}
|
||||
|
||||
// console.warn('lxc needs updating', sdk.clusterManager.getClusterWorkerId());
|
||||
// console.warn(foundDockerComposeSh);
|
||||
await fs.promises.copyFile(LXC_DOCKER_COMPOSE_SH_PATH, DOCKER_COMPOSE_SH_PATH);
|
||||
await fs.promises.chmod(DOCKER_COMPOSE_SH_PATH, 0o755);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import sdk, { DeviceCreator, DeviceCreatorSettings, DeviceProvider, Readme, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedUser, ScryptedUserAccessControl, Setting, Settings, SettingValue } from "@scrypted/sdk";
|
||||
import sdk, { DeviceCreator, DeviceCreatorSettings, DeviceManifest, DeviceProvider, Readme, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedUser, ScryptedUserAccessControl, Setting, Settings, SettingValue } from "@scrypted/sdk";
|
||||
import { addAccessControlsForInterface } from "@scrypted/sdk/acl";
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
export const UsersNativeId = 'users';
|
||||
@@ -132,7 +132,13 @@ export class UsersCore extends ScryptedDeviceBase implements Readme, DeviceProvi
|
||||
deviceCreator: 'Scrypted User',
|
||||
};
|
||||
|
||||
this.syncUsers();
|
||||
this.syncUsers()
|
||||
.then(length => {
|
||||
if (!length) {
|
||||
this.console.log('no users found, looping for first user');
|
||||
setInterval(() => this.syncUsers(), 60 * 1000);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async getDevice(nativeId: string): Promise<any> {
|
||||
@@ -192,7 +198,7 @@ export class UsersCore extends ScryptedDeviceBase implements Readme, DeviceProvi
|
||||
async syncUsers() {
|
||||
const usersService = await sdk.systemManager.getComponent('users');
|
||||
const users: DBUser[] = await usersService.getAllUsers();
|
||||
await sdk.deviceManager.onDevicesChanged({
|
||||
const manifest: DeviceManifest = {
|
||||
providerNativeId: this.nativeId,
|
||||
devices: users.map(user => ({
|
||||
name: user.username,
|
||||
@@ -203,6 +209,16 @@ export class UsersCore extends ScryptedDeviceBase implements Readme, DeviceProvi
|
||||
],
|
||||
type: ScryptedDeviceType.Person,
|
||||
})),
|
||||
})
|
||||
};
|
||||
const nativeIds = new Set(manifest.devices.map(d => d.nativeId));
|
||||
for (const nativeId of sdk.deviceManager.getNativeIds()) {
|
||||
nativeIds.delete(nativeId);
|
||||
}
|
||||
if (nativeIds.size) {
|
||||
// add any missing users.
|
||||
await sdk.deviceManager.onDevicesChanged(manifest);
|
||||
}
|
||||
|
||||
return manifest.devices.length;
|
||||
}
|
||||
}
|
||||
|
||||
58
plugins/coreml/package-lock.json
generated
58
plugins/coreml/package-lock.json
generated
@@ -1,34 +1,42 @@
|
||||
{
|
||||
"name": "@scrypted/coreml",
|
||||
"version": "0.1.77",
|
||||
"version": "0.1.83",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/coreml",
|
||||
"version": "0.1.77",
|
||||
"version": "0.1.83",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.77",
|
||||
"version": "0.5.22",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@rollup/plugin-commonjs": "^28.0.5",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.7",
|
||||
"babel-loader": "^9.2.1",
|
||||
"axios": "^1.10.0",
|
||||
"babel-loader": "^10.0.0",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"openai": "^5.3.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.43.0",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.5.4",
|
||||
"webpack": "^5.95.0",
|
||||
"ts-loader": "^9.5.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.8.3",
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
},
|
||||
"bin": {
|
||||
@@ -41,11 +49,9 @@
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/stringify-object": "^4.0.5",
|
||||
"stringify-object": "^3.3.0",
|
||||
"@types/node": "^24.0.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.10"
|
||||
"typedoc": "^0.28.5"
|
||||
}
|
||||
},
|
||||
"../sdk": {
|
||||
@@ -60,23 +66,29 @@
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/stringify-object": "^4.0.5",
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@rollup/plugin-commonjs": "^28.0.5",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"@types/node": "^24.0.1",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.7",
|
||||
"babel-loader": "^9.2.1",
|
||||
"axios": "^1.10.0",
|
||||
"babel-loader": "^10.0.0",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"openai": "^5.3.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"stringify-object": "^3.3.0",
|
||||
"rollup": "^4.43.0",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-loader": "^9.5.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.10",
|
||||
"typescript": "^5.5.4",
|
||||
"webpack": "^5.95.0",
|
||||
"tslib": "^2.8.1",
|
||||
"typedoc": "^0.28.5",
|
||||
"typescript": "^5.8.3",
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
"runtime": "python",
|
||||
"type": "API",
|
||||
"interfaces": [
|
||||
"ScryptedSystemDevice",
|
||||
"DeviceCreator",
|
||||
"Settings",
|
||||
"DeviceProvider",
|
||||
"ClusterForkInterface",
|
||||
@@ -48,5 +50,5 @@
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.1.77"
|
||||
"version": "0.1.83"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ from scrypted_sdk import Setting, SettingValue
|
||||
|
||||
from common import yolo
|
||||
from coreml.face_recognition import CoreMLFaceRecognition
|
||||
from coreml.custom_detection import CoreMLCustomDetection
|
||||
from coreml.clip_embedding import CoreMLClipEmbedding
|
||||
|
||||
try:
|
||||
from coreml.text_recognition import CoreMLTextRecognition
|
||||
@@ -77,6 +79,8 @@ class CoreMLPlugin(
|
||||
def __init__(self, nativeId: str | None = None, forked: bool = False):
|
||||
super().__init__(nativeId=nativeId, forked=forked)
|
||||
|
||||
self.custom_models = {}
|
||||
|
||||
model = self.storage.getItem("model") or "Default"
|
||||
if model == "Default" or model not in availableModels:
|
||||
if model != "Default":
|
||||
@@ -143,13 +147,14 @@ class CoreMLPlugin(
|
||||
|
||||
self.faceDevice = None
|
||||
self.textDevice = None
|
||||
self.clipDevice = None
|
||||
|
||||
if not self.forked:
|
||||
asyncio.ensure_future(self.prepareRecognitionModels(), loop=self.loop)
|
||||
|
||||
async def prepareRecognitionModels(self):
|
||||
try:
|
||||
devices = [
|
||||
await scrypted_sdk.deviceManager.onDeviceDiscovered(
|
||||
{
|
||||
"nativeId": "facerecognition",
|
||||
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
|
||||
@@ -159,10 +164,10 @@ class CoreMLPlugin(
|
||||
],
|
||||
"name": "CoreML Face Recognition",
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
if CoreMLTextRecognition:
|
||||
devices.append(
|
||||
await scrypted_sdk.deviceManager.onDeviceDiscovered(
|
||||
{
|
||||
"nativeId": "textrecognition",
|
||||
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
|
||||
@@ -174,9 +179,17 @@ class CoreMLPlugin(
|
||||
},
|
||||
)
|
||||
|
||||
await scrypted_sdk.deviceManager.onDevicesChanged(
|
||||
await scrypted_sdk.deviceManager.onDeviceDiscovered(
|
||||
{
|
||||
"devices": devices,
|
||||
"nativeId": "clipembedding",
|
||||
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
|
||||
"interfaces": [
|
||||
scrypted_sdk.ScryptedInterface.ClusterForkInterface.value,
|
||||
scrypted_sdk.ScryptedInterface.ObjectDetection.value,
|
||||
scrypted_sdk.ScryptedInterface.TextEmbedding.value,
|
||||
scrypted_sdk.ScryptedInterface.ImageEmbedding.value,
|
||||
],
|
||||
"name": "CoreML CLIP Embedding",
|
||||
}
|
||||
)
|
||||
except:
|
||||
@@ -186,10 +199,19 @@ class CoreMLPlugin(
|
||||
if nativeId == "facerecognition":
|
||||
self.faceDevice = self.faceDevice or CoreMLFaceRecognition(self, nativeId)
|
||||
return self.faceDevice
|
||||
if nativeId == "textrecognition":
|
||||
elif nativeId == "textrecognition":
|
||||
self.textDevice = self.textDevice or CoreMLTextRecognition(self, nativeId)
|
||||
return self.textDevice
|
||||
raise Exception("unknown device")
|
||||
elif nativeId == "clipembedding":
|
||||
self.clipDevice = self.clipDevice or CoreMLClipEmbedding(self, nativeId)
|
||||
return self.clipDevice
|
||||
custom_model = self.custom_models.get(nativeId, None)
|
||||
if custom_model:
|
||||
return custom_model
|
||||
custom_model = CoreMLCustomDetection(self, nativeId)
|
||||
self.custom_models[nativeId] = custom_model
|
||||
await custom_model.reportDevice(nativeId, custom_model.providedName)
|
||||
return custom_model
|
||||
|
||||
async def getSettings(self) -> list[Setting]:
|
||||
model = self.storage.getItem("model") or "Default"
|
||||
|
||||
85
plugins/coreml/src/coreml/clip_embedding.py
Normal file
85
plugins/coreml/src/coreml/clip_embedding.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import coremltools as ct
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from scrypted_sdk import ObjectsDetected
|
||||
|
||||
from predict.clip import ClipEmbedding
|
||||
|
||||
|
||||
class CoreMLClipEmbedding(ClipEmbedding):
|
||||
def __init__(self, plugin, nativeId: str):
|
||||
super().__init__(plugin=plugin, nativeId=nativeId)
|
||||
self.predictExecutor = concurrent.futures.ThreadPoolExecutor(1, "predict-clip")
|
||||
|
||||
def getFiles(self):
|
||||
return [
|
||||
"text.mlpackage/Manifest.json",
|
||||
"text.mlpackage/Data/com.apple.CoreML/weights/weight.bin",
|
||||
"text.mlpackage/Data/com.apple.CoreML/model.mlmodel",
|
||||
|
||||
"vision.mlpackage/Manifest.json",
|
||||
"vision.mlpackage/Data/com.apple.CoreML/weights/weight.bin",
|
||||
"vision.mlpackage/Data/com.apple.CoreML/model.mlmodel",
|
||||
]
|
||||
|
||||
def loadModel(self, files):
|
||||
# find the xml file in the files list
|
||||
text_manifest = [f for f in files if f.lower().endswith('text.mlpackage/manifest.json')]
|
||||
if not text_manifest:
|
||||
raise ValueError("No XML model file found in the provided files list")
|
||||
text_manifest = text_manifest[0]
|
||||
|
||||
vision_manifest = [f for f in files if f.lower().endswith('vision.mlpackage/manifest.json')]
|
||||
if not vision_manifest:
|
||||
raise ValueError("No XML model file found in the provided files list")
|
||||
vision_manifest = vision_manifest[0]
|
||||
|
||||
|
||||
textModel = ct.models.MLModel(os.path.dirname(text_manifest))
|
||||
visionModel = ct.models.MLModel(os.path.dirname(vision_manifest))
|
||||
|
||||
return textModel, visionModel
|
||||
|
||||
async def detect_once(self, input: Image.Image, settings: Any, src_size, cvss):
|
||||
def predict():
|
||||
inputs = self.processor(images=input, return_tensors="np", padding="max_length", truncation=True)
|
||||
_, vision_model = self.model
|
||||
vision_predictions = vision_model.predict({'x': inputs['pixel_values']})
|
||||
image_embeds = vision_predictions['var_877']
|
||||
# this is a hack to utilize the existing image massaging infrastructure
|
||||
embedding = bytearray(image_embeds.astype(np.float32).tobytes())
|
||||
ret: ObjectsDetected = {
|
||||
"detections": [
|
||||
{
|
||||
"embedding": embedding,
|
||||
}
|
||||
],
|
||||
"inputDimensions": src_size
|
||||
}
|
||||
|
||||
return ret
|
||||
|
||||
ret = await asyncio.get_event_loop().run_in_executor(
|
||||
self.predictExecutor, lambda: predict()
|
||||
)
|
||||
return ret
|
||||
|
||||
async def getTextEmbedding(self, input):
|
||||
def predict():
|
||||
inputs = self.processor(text=input, return_tensors="np", padding="max_length", truncation=True)
|
||||
text_model, _ = self.model
|
||||
text_predictions = text_model.predict({'input_ids_1': inputs['input_ids'].astype(np.float32), 'attention_mask_1': inputs['attention_mask'].astype(np.float32)})
|
||||
text_embeds = text_predictions['var_1050']
|
||||
return bytearray(text_embeds.astype(np.float32).tobytes())
|
||||
|
||||
ret = await asyncio.get_event_loop().run_in_executor(
|
||||
self.predictExecutor, lambda: predict()
|
||||
)
|
||||
return ret
|
||||
60
plugins/coreml/src/coreml/custom_detection.py
Normal file
60
plugins/coreml/src/coreml/custom_detection.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
import os
|
||||
|
||||
import coremltools as ct
|
||||
import numpy as np
|
||||
import scrypted_sdk
|
||||
from PIL import Image
|
||||
|
||||
from predict.custom_detect import CustomDetection
|
||||
|
||||
|
||||
class CoreMLCustomDetection(CustomDetection):
|
||||
def __init__(self, plugin, nativeId: str):
|
||||
super().__init__(plugin=plugin, nativeId=nativeId)
|
||||
self.prefer_relu = True
|
||||
self.detectExecutor = concurrent.futures.ThreadPoolExecutor(1, "detect-custom")
|
||||
|
||||
def loadModel(self, files: list[str]):
|
||||
# find the xml file in the files list
|
||||
manifest_files = [f for f in files if f.lower().endswith('manifest.json')]
|
||||
if not manifest_files:
|
||||
raise ValueError("No Manifest.json file found in the provided files list")
|
||||
manifest_file = manifest_files[0]
|
||||
modelFile = os.path.dirname(manifest_file)
|
||||
|
||||
model = ct.models.MLModel(modelFile)
|
||||
inputName = model.get_spec().description.input[0].name
|
||||
return model, inputName
|
||||
|
||||
async def predictModel(self, input: Image.Image) -> scrypted_sdk.ObjectsDetected:
|
||||
model, inputName = self.model
|
||||
def predict():
|
||||
if self.model_config.get("mean", None) and self.model_config.get("std", None):
|
||||
im = np.array(input)
|
||||
im = im.astype(np.float32) / 255.0
|
||||
|
||||
mean = np.array(self.model_config.get("mean", None), dtype=np.float32)
|
||||
std = np.array(self.model_config.get("std", None), dtype=np.float32)
|
||||
im = (im - mean) / std
|
||||
|
||||
# Convert HWC to CHW
|
||||
im = im.transpose(2, 0, 1) # Channels first
|
||||
im = im.astype(np.float32)
|
||||
im = np.ascontiguousarray(im)
|
||||
im = np.expand_dims(im, axis=0)
|
||||
|
||||
out_dict = model.predict({inputName: im})
|
||||
else:
|
||||
out_dict = model.predict({inputName: input})
|
||||
|
||||
results = list(out_dict.values())[0][0]
|
||||
return results
|
||||
|
||||
results = await asyncio.get_event_loop().run_in_executor(
|
||||
self.detectExecutor, lambda: predict()
|
||||
)
|
||||
return results
|
||||
@@ -1,3 +1,5 @@
|
||||
coremltools==8.0
|
||||
Pillow==10.3.0
|
||||
opencv-python-headless==4.10.0.84
|
||||
|
||||
transformers==4.52.4
|
||||
|
||||
@@ -522,32 +522,74 @@ export class HikvisionCameraAPI implements HikvisionAPI {
|
||||
|
||||
async setSupplementLight(params: { on?: boolean, brightness?: number, mode?: 'auto' | 'manual' }): Promise<void> {
|
||||
const { json } = await this.getSupplementLight();
|
||||
|
||||
if (json.ResponseStatus) {
|
||||
throw new Error("Supplemental light is not supported on this device.");
|
||||
}
|
||||
|
||||
const supp: any = json.SupplementLight;
|
||||
if (!supp) {
|
||||
throw new Error("Supplemental light configuration not available.");
|
||||
}
|
||||
|
||||
if (supp.supplementLightMode && supp.supplementLightMode.opt) {
|
||||
const availableModes = supp.supplementLightMode.opt.split(',').map(s => s.trim());
|
||||
const selectedMode = params.on
|
||||
? (availableModes.find(mode => mode.toLowerCase() !== 'close') || 'close')
|
||||
: 'close';
|
||||
supp.supplementLightMode = [selectedMode];
|
||||
const getCurrentValue = (obj: any) => Array.isArray(obj) ? obj[0] : obj;
|
||||
const setValue = (t: any, k: string, v: string) => {
|
||||
t[k] = Array.isArray(t[k]) ? [v] : v;
|
||||
};
|
||||
|
||||
const setBrightnessForMode = (level: number, mode: string) => {
|
||||
const v = level.toString();
|
||||
const map: Record<string, Array<{ obj: any; key: string }>> = {
|
||||
colorVuWhiteLight: [
|
||||
{ obj: supp, key: 'whiteLightBrightness' },
|
||||
{ obj: supp.colorVuWhiteLightModeCfg, key: 'whiteLightbrightLimit' }
|
||||
],
|
||||
irLight: [
|
||||
{ obj: supp, key: 'irLightBrightness' },
|
||||
{ obj: supp.IrLightModeCfg, key: 'irLightbrightLimit' }
|
||||
],
|
||||
eventIntelligence: [
|
||||
{ obj: supp.EventIntelligenceModeCfg, key: 'whiteLightBrightness' },
|
||||
{ obj: supp.EventIntelligenceModeCfg, key: 'irLightBrightness' }
|
||||
]
|
||||
};
|
||||
(map[mode] || []).forEach(({ obj, key }) => {
|
||||
if (obj && obj[key] !== undefined) setValue(obj, key, v);
|
||||
});
|
||||
};
|
||||
|
||||
const setModeConfigs = (m: 'auto' | 'manual') => {
|
||||
if (getCurrentValue(supp.supplementLightMode) === 'eventIntelligence' && supp.EventIntelligenceModeCfg) {
|
||||
setValue(supp.EventIntelligenceModeCfg, 'brightnessRegulatMode', m);
|
||||
} else if (supp.mixedLightBrightnessRegulatMode !== undefined) {
|
||||
setValue(supp, 'mixedLightBrightnessRegulatMode', m);
|
||||
} else if (supp.isAutoModeBrightnessCfg !== undefined) {
|
||||
setValue(supp, 'isAutoModeBrightnessCfg', m === 'auto' ? 'true' : 'false');
|
||||
}
|
||||
};
|
||||
|
||||
if (params.on !== undefined && supp.supplementLightMode) {
|
||||
const opts = supp.supplementLightMode.opt?.split(',').map((s: string) => s.trim()) || [];
|
||||
this.console.log('[API] Available supplemental light modes:', opts);
|
||||
if (params.on) {
|
||||
const preferred = ['colorVuWhiteLight', 'eventIntelligence', 'irLight'];
|
||||
const sel = preferred.find(m => opts.includes(m));
|
||||
if (!sel) {
|
||||
throw new Error(`Cannot turn on: no supported mode. Available: ${opts.join(', ')}`);
|
||||
}
|
||||
setValue(supp, 'supplementLightMode', sel);
|
||||
} else {
|
||||
setValue(supp, 'supplementLightMode', 'close');
|
||||
}
|
||||
}
|
||||
|
||||
if (params.mode) {
|
||||
supp.mixedLightBrightnessRegulatMode = [params.mode];
|
||||
} else if (params.on !== undefined) {
|
||||
supp.mixedLightBrightnessRegulatMode = [params.on ? "manual" : "auto"];
|
||||
setModeConfigs(params.mode);
|
||||
}
|
||||
|
||||
if (params.brightness !== undefined) {
|
||||
let brightness = Math.max(0, Math.min(100, params.brightness));
|
||||
supp.whiteLightBrightness = [brightness.toString()];
|
||||
const lvl = Math.min(100, Math.max(0, params.brightness));
|
||||
const mode = getCurrentValue(supp.supplementLightMode);
|
||||
if (mode !== 'close') {
|
||||
setBrightnessForMode(lvl, mode);
|
||||
} else {
|
||||
this.console.warn('[API] Brightness change ignored: light is off');
|
||||
}
|
||||
}
|
||||
|
||||
const builder = new xml2js.Builder({
|
||||
@@ -555,7 +597,7 @@ export class HikvisionCameraAPI implements HikvisionAPI {
|
||||
renderOpts: { pretty: false },
|
||||
});
|
||||
const newXml = builder.buildObject({ SupplementLight: supp });
|
||||
|
||||
|
||||
await this.request({
|
||||
method: 'PUT',
|
||||
url: `http://${this.ip}/ISAPI/Image/channels/1/supplementLight`,
|
||||
|
||||
2
plugins/ncnn/.vscode/settings.json
vendored
2
plugins/ncnn/.vscode/settings.json
vendored
@@ -1,6 +1,6 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "scrypted-amd",
|
||||
"scrypted.debugHost": "scrypted-nvr",
|
||||
"python.analysis.extraPaths": [
|
||||
"./node_modules/@scrypted/sdk/types/scrypted_python"
|
||||
]
|
||||
|
||||
4
plugins/ncnn/package-lock.json
generated
4
plugins/ncnn/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/ncnn",
|
||||
"version": "0.1.82",
|
||||
"version": "0.1.88",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/ncnn",
|
||||
"version": "0.1.82",
|
||||
"version": "0.1.88",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
"runtime": "python",
|
||||
"type": "API",
|
||||
"interfaces": [
|
||||
"ScryptedSystemDevice",
|
||||
"DeviceCreator",
|
||||
"Settings",
|
||||
"DeviceProvider",
|
||||
"ClusterForkInterface",
|
||||
@@ -48,5 +50,5 @@
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.1.82"
|
||||
"version": "0.1.88"
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ from scrypted_sdk import Setting, SettingValue
|
||||
import ncnn
|
||||
from common import yolo
|
||||
|
||||
from .custom_detection import NCNNCustomDetection
|
||||
try:
|
||||
from nc.face_recognition import NCNNFaceRecognition
|
||||
except:
|
||||
@@ -78,6 +79,8 @@ class NCNNPlugin(
|
||||
def __init__(self, nativeId: str | None = None, forked: bool = False):
|
||||
super().__init__(nativeId=nativeId, forked=forked)
|
||||
|
||||
self.custom_models = {}
|
||||
|
||||
model = self.storage.getItem("model") or "Default"
|
||||
if model == "Default" or model not in availableModels:
|
||||
if model != "Default":
|
||||
@@ -118,9 +121,9 @@ class NCNNPlugin(
|
||||
|
||||
self.net = ncnn.Net()
|
||||
self.net.opt.use_vulkan_compute = True
|
||||
self.net.opt.use_fp16_packed = False
|
||||
self.net.opt.use_fp16_storage = False
|
||||
self.net.opt.use_fp16_arithmetic = False
|
||||
# self.net.opt.use_fp16_packed = False
|
||||
# self.net.opt.use_fp16_storage = False
|
||||
# self.net.opt.use_fp16_arithmetic = False
|
||||
|
||||
self.net.load_param(paramFile)
|
||||
self.net.load_model(binFile)
|
||||
@@ -151,7 +154,7 @@ class NCNNPlugin(
|
||||
|
||||
async def prepareRecognitionModels(self):
|
||||
try:
|
||||
devices = [
|
||||
await scrypted_sdk.deviceManager.onDeviceDiscovered(
|
||||
{
|
||||
"nativeId": "facerecognition",
|
||||
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
|
||||
@@ -161,10 +164,10 @@ class NCNNPlugin(
|
||||
],
|
||||
"name": "NCNN Face Recognition",
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
if NCNNTextRecognition:
|
||||
devices.append(
|
||||
await scrypted_sdk.deviceManager.onDeviceDiscovered(
|
||||
{
|
||||
"nativeId": "textrecognition",
|
||||
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
|
||||
@@ -175,12 +178,6 @@ class NCNNPlugin(
|
||||
"name": "NCNN Text Recognition",
|
||||
},
|
||||
)
|
||||
|
||||
await scrypted_sdk.deviceManager.onDevicesChanged(
|
||||
{
|
||||
"devices": devices,
|
||||
}
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -191,7 +188,13 @@ class NCNNPlugin(
|
||||
if nativeId == "textrecognition":
|
||||
self.textDevice = self.textDevice or NCNNTextRecognition(self, nativeId)
|
||||
return self.textDevice
|
||||
raise Exception("unknown device")
|
||||
custom_model = self.custom_models.get(nativeId, None)
|
||||
if custom_model:
|
||||
return custom_model
|
||||
custom_model = NCNNCustomDetection(self, nativeId)
|
||||
self.custom_models[nativeId] = custom_model
|
||||
await custom_model.reportDevice(nativeId, custom_model.providedName)
|
||||
return custom_model
|
||||
|
||||
async def getSettings(self) -> list[Setting]:
|
||||
model = self.storage.getItem("model") or "Default"
|
||||
|
||||
82
plugins/ncnn/src/nc/custom_detection.py
Normal file
82
plugins/ncnn/src/nc/custom_detection.py
Normal file
@@ -0,0 +1,82 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
import os
|
||||
import ncnn
|
||||
|
||||
from predict.custom_detect import CustomDetection
|
||||
from scrypted_sdk import ObjectsDetected
|
||||
import concurrent.futures
|
||||
|
||||
|
||||
class NCNNCustomDetection(CustomDetection):
|
||||
def __init__(self, plugin, nativeId: str):
|
||||
super().__init__(plugin=plugin, nativeId=nativeId)
|
||||
self.prefer_relu = True
|
||||
self.detectExecutor = concurrent.futures.ThreadPoolExecutor(1, "detect-custom")
|
||||
self.prepareExecutor = concurrent.futures.ThreadPoolExecutor(1, "prepare-custom")
|
||||
|
||||
def loadModel(self, files: list[str]):
|
||||
# find the xml file in the files list
|
||||
bin_files = [f for f in files if f.lower().endswith('.bin')]
|
||||
if not bin_files:
|
||||
raise ValueError("No bkin file found in the provided files list")
|
||||
bin_file = bin_files[0]
|
||||
param_files = [f for f in files if f.lower().endswith('.param')]
|
||||
if not param_files:
|
||||
raise ValueError("No param file found in the provided files list")
|
||||
param_file = param_files[0]
|
||||
|
||||
net = ncnn.Net()
|
||||
net.opt.use_vulkan_compute = True
|
||||
# net.opt.use_fp16_packed = False
|
||||
# net.opt.use_fp16_storage = False
|
||||
# net.opt.use_fp16_arithmetic = False
|
||||
|
||||
net.load_param(param_file)
|
||||
net.load_model(bin_file)
|
||||
|
||||
input_name = net.input_names()[0]
|
||||
|
||||
return net, input_name
|
||||
|
||||
async def predictModel(self, input: Image.Image) -> ObjectsDetected:
|
||||
def prepare():
|
||||
im = np.expand_dims(input, axis=0)
|
||||
im = im.transpose((0, 3, 1, 2)) # BHWC to BCHW, (n, 3, h, w)
|
||||
im = im.astype(np.float32) / 255.0
|
||||
|
||||
if self.model_config.get("mean", None) and self.model_config.get("std", None):
|
||||
mean = np.array(self.model_config["mean"])
|
||||
std = np.array(self.model_config["std"])
|
||||
mean = mean.reshape(1, -1, 1, 1)
|
||||
std = std.reshape(1, -1, 1, 1)
|
||||
im = (im - mean) / std
|
||||
im = im.astype(np.float32)
|
||||
|
||||
# no batch? https://github.com/Tencent/ncnn/issues/5990#issuecomment-2832927105
|
||||
im = im.squeeze(0)
|
||||
im = np.ascontiguousarray(im) # contiguous
|
||||
return im
|
||||
|
||||
def predict(input_tensor):
|
||||
net, input_name = self.model
|
||||
input_ncnn = ncnn.Mat(input_tensor)
|
||||
ex = net.create_extractor()
|
||||
ex.input(input_name, input_ncnn)
|
||||
|
||||
output_ncnn = ncnn.Mat()
|
||||
ex.extract("out0", output_ncnn)
|
||||
|
||||
output_tensors = np.array(output_ncnn)
|
||||
return output_tensors
|
||||
|
||||
input_tensor = await asyncio.get_event_loop().run_in_executor(
|
||||
self.prepareExecutor, lambda: prepare()
|
||||
)
|
||||
return await asyncio.get_event_loop().run_in_executor(
|
||||
self.detectExecutor, lambda: predict(input_tensor)
|
||||
)
|
||||
@@ -42,9 +42,9 @@ class NCNNFaceRecognition(FaceRecognizeDetection):
|
||||
|
||||
net = ncnn.Net()
|
||||
net.opt.use_vulkan_compute = True
|
||||
net.opt.use_fp16_packed = False
|
||||
net.opt.use_fp16_storage = False
|
||||
net.opt.use_fp16_arithmetic = False
|
||||
# net.opt.use_fp16_packed = False
|
||||
# net.opt.use_fp16_storage = False
|
||||
# net.opt.use_fp16_arithmetic = False
|
||||
|
||||
net.load_param(paramFile)
|
||||
net.load_model(binFile)
|
||||
|
||||
@@ -35,9 +35,9 @@ class NCNNTextRecognition(TextRecognition):
|
||||
|
||||
net = ncnn.Net()
|
||||
net.opt.use_vulkan_compute = True
|
||||
net.opt.use_fp16_packed = False
|
||||
net.opt.use_fp16_storage = False
|
||||
net.opt.use_fp16_arithmetic = False
|
||||
# net.opt.use_fp16_packed = False
|
||||
# net.opt.use_fp16_storage = False
|
||||
# net.opt.use_fp16_arithmetic = False
|
||||
|
||||
net.load_param(paramFile)
|
||||
net.load_model(binFile)
|
||||
|
||||
@@ -9,7 +9,8 @@ import { FFmpegVideoFrameGenerator } from './ffmpeg-videoframes';
|
||||
import { fixLegacyClipPath, normalizeBox, polygonContainsBoundingBox, polygonIntersectsBoundingBox } from './polygon';
|
||||
import { SMART_MOTIONSENSOR_PREFIX, SmartMotionSensor } from './smart-motionsensor';
|
||||
import { SMART_OCCUPANCYSENSOR_PREFIX, SmartOccupancySensor } from './smart-occupancy-sensor';
|
||||
import { getAllDevices, safeParseJson } from './util';
|
||||
import { safeParseJson } from '../../../common/src/json';
|
||||
import { getAllDevices } from '../../../common/src/devices';
|
||||
import { FFmpegAudioDetectionMixinProvider } from './ffmpeg-audiosensor';
|
||||
|
||||
|
||||
@@ -94,7 +95,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
onGet: async () => {
|
||||
const choices = [
|
||||
'Default',
|
||||
...getAllDevices().filter(d => d.interfaces.includes(ScryptedInterface.VideoFrameGenerator)).map(d => d.name),
|
||||
...getAllDevices(sdk.systemManager).filter(d => d.interfaces.includes(ScryptedInterface.VideoFrameGenerator)).map(d => d.name),
|
||||
];
|
||||
return {
|
||||
hide: this.model?.decoder,
|
||||
@@ -690,7 +691,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
if (frameGenerator === 'Default')
|
||||
frameGenerator = this.plugin.storageSettings.values.defaultDecoder || 'Default';
|
||||
|
||||
const pipelines = getAllDevices().filter(d => d.interfaces.includes(ScryptedInterface.VideoFrameGenerator));
|
||||
const pipelines = getAllDevices(sdk.systemManager).filter(d => d.interfaces.includes(ScryptedInterface.VideoFrameGenerator));
|
||||
const webassembly = sdk.systemManager.getDeviceById('@scrypted/nvr', 'decoder') || undefined;
|
||||
const gstreamer = sdk.systemManager.getDeviceById('@scrypted/python-codecs', 'gstreamer') || undefined;
|
||||
const libav = sdk.systemManager.getDeviceById('@scrypted/python-codecs', 'libav') || undefined;
|
||||
@@ -1026,7 +1027,7 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
|
||||
onGet: async () => {
|
||||
const choices = [
|
||||
'Default',
|
||||
...getAllDevices().filter(d => d.interfaces.includes(ScryptedInterface.VideoFrameGenerator)).map(d => d.name),
|
||||
...getAllDevices(sdk.systemManager).filter(d => d.interfaces.includes(ScryptedInterface.VideoFrameGenerator)).map(d => d.name),
|
||||
];
|
||||
return {
|
||||
choices,
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import sdk from '@scrypted/sdk';
|
||||
|
||||
export function safeParseJson(value: string) {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getAllDevices() {
|
||||
return Object.keys(sdk.systemManager.getSystemState()).map(id => sdk.systemManager.getDeviceById(id));
|
||||
}
|
||||
20
plugins/onnx/.vscode/settings.json
vendored
20
plugins/onnx/.vscode/settings.json
vendored
@@ -1,20 +1,4 @@
|
||||
|
||||
{
|
||||
// docker installation
|
||||
"scrypted.debugHost": "scrypted-nvr",
|
||||
"scrypted.serverRoot": "/server",
|
||||
|
||||
// pi local installation
|
||||
// "scrypted.debugHost": "192.168.2.119",
|
||||
// "scrypted.serverRoot": "/home/pi/.scrypted",
|
||||
|
||||
// local checkout
|
||||
// "scrypted.debugHost": "127.0.0.1",
|
||||
// "scrypted.serverRoot": "/Users/koush/.scrypted",
|
||||
// "scrypted.debugHost": "koushik-winvm",
|
||||
// "scrypted.serverRoot": "C:\\Users\\koush\\.scrypted",
|
||||
|
||||
"python.analysis.extraPaths": [
|
||||
"./node_modules/@scrypted/sdk/types/scrypted_python"
|
||||
]
|
||||
}
|
||||
"scrypted.debugHost": "koushik-ubuntu24",
|
||||
}
|
||||
4
plugins/onnx/package-lock.json
generated
4
plugins/onnx/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/onnx",
|
||||
"version": "0.1.120",
|
||||
"version": "0.1.127",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/onnx",
|
||||
"version": "0.1.120",
|
||||
"version": "0.1.127",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
"runtime": "python",
|
||||
"type": "API",
|
||||
"interfaces": [
|
||||
"ScryptedSystemDevice",
|
||||
"DeviceCreator",
|
||||
"DeviceProvider",
|
||||
"Settings",
|
||||
"ClusterForkInterface",
|
||||
@@ -48,5 +50,5 @@
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.1.120"
|
||||
"version": "0.1.127"
|
||||
}
|
||||
|
||||
@@ -17,10 +17,12 @@ from PIL import Image
|
||||
from scrypted_sdk.other import SettingValue
|
||||
from scrypted_sdk.types import Setting
|
||||
|
||||
from .custom_detection import ONNXCustomDetection
|
||||
import common.yolo as yolo
|
||||
from predict import PredictPlugin
|
||||
|
||||
from .face_recognition import ONNXFaceRecognition
|
||||
from .clip_embedding import ONNXClipEmbedding
|
||||
|
||||
try:
|
||||
from .text_recognition import ONNXTextRecognition
|
||||
@@ -58,6 +60,8 @@ class ONNXPlugin(
|
||||
def __init__(self, nativeId: str | None = None, forked: bool = False):
|
||||
super().__init__(nativeId=nativeId, forked=forked)
|
||||
|
||||
self.custom_models = {}
|
||||
|
||||
model = self.storage.getItem("model") or "Default"
|
||||
if model == "Default" or model not in availableModels:
|
||||
if model != "Default":
|
||||
@@ -162,13 +166,14 @@ class ONNXPlugin(
|
||||
|
||||
self.faceDevice = None
|
||||
self.textDevice = None
|
||||
self.clipDevice = None
|
||||
|
||||
if not self.forked:
|
||||
asyncio.ensure_future(self.prepareRecognitionModels(), loop=self.loop)
|
||||
|
||||
async def prepareRecognitionModels(self):
|
||||
try:
|
||||
devices = [
|
||||
await scrypted_sdk.deviceManager.onDeviceDiscovered(
|
||||
{
|
||||
"nativeId": "facerecognition",
|
||||
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
|
||||
@@ -178,10 +183,10 @@ class ONNXPlugin(
|
||||
],
|
||||
"name": "ONNX Face Recognition",
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
if ONNXTextRecognition:
|
||||
devices.append(
|
||||
await scrypted_sdk.deviceManager.onDeviceDiscovered(
|
||||
{
|
||||
"nativeId": "textrecognition",
|
||||
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
|
||||
@@ -193,9 +198,17 @@ class ONNXPlugin(
|
||||
},
|
||||
)
|
||||
|
||||
await scrypted_sdk.deviceManager.onDevicesChanged(
|
||||
await scrypted_sdk.deviceManager.onDeviceDiscovered(
|
||||
{
|
||||
"devices": devices,
|
||||
"nativeId": "clipembedding",
|
||||
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
|
||||
"interfaces": [
|
||||
scrypted_sdk.ScryptedInterface.ClusterForkInterface.value,
|
||||
scrypted_sdk.ScryptedInterface.ObjectDetection.value,
|
||||
scrypted_sdk.ScryptedInterface.TextEmbedding.value,
|
||||
scrypted_sdk.ScryptedInterface.ImageEmbedding.value,
|
||||
],
|
||||
"name": "ONNX CLIP Embedding",
|
||||
}
|
||||
)
|
||||
except:
|
||||
@@ -208,7 +221,16 @@ class ONNXPlugin(
|
||||
elif nativeId == "textrecognition":
|
||||
self.textDevice = self.textDevice or ONNXTextRecognition(self, nativeId)
|
||||
return self.textDevice
|
||||
raise Exception("unknown device")
|
||||
elif nativeId == "clipembedding":
|
||||
self.clipDevice = self.clipDevice or ONNXClipEmbedding(self, nativeId)
|
||||
return self.clipDevice
|
||||
custom_model = self.custom_models.get(nativeId, None)
|
||||
if custom_model:
|
||||
return custom_model
|
||||
custom_model = ONNXCustomDetection(self, nativeId)
|
||||
self.custom_models[nativeId] = custom_model
|
||||
await custom_model.reportDevice(nativeId, custom_model.providedName)
|
||||
return custom_model
|
||||
|
||||
async def getSettings(self) -> list[Setting]:
|
||||
model = self.storage.getItem("model") or "Default"
|
||||
|
||||
122
plugins/onnx/src/ort/clip_embedding.py
Normal file
122
plugins/onnx/src/ort/clip_embedding.py
Normal file
@@ -0,0 +1,122 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import onnxruntime
|
||||
from PIL import Image
|
||||
import threading
|
||||
|
||||
from predict.clip import ClipEmbedding
|
||||
from scrypted_sdk import ObjectsDetected
|
||||
import concurrent.futures
|
||||
import sys
|
||||
import platform
|
||||
|
||||
|
||||
class ONNXClipEmbedding(ClipEmbedding):
|
||||
def __init__(self, plugin, nativeId: str):
|
||||
super().__init__(plugin=plugin, nativeId=nativeId)
|
||||
|
||||
def getFiles(self):
|
||||
return [
|
||||
"text.onnx",
|
||||
"vision.onnx",
|
||||
]
|
||||
|
||||
def loadModel(self, files):
|
||||
# find the xml file in the files list
|
||||
text_onnx = [f for f in files if f.lower().endswith('text.onnx')]
|
||||
if not text_onnx:
|
||||
raise ValueError("No onnx model file found in the provided files list")
|
||||
text_onnx = text_onnx[0]
|
||||
|
||||
vision_onnx = [f for f in files if f.lower().endswith('vision.onnx')]
|
||||
if not vision_onnx:
|
||||
raise ValueError("No onnx model file found in the provided files list")
|
||||
vision_onnx = vision_onnx[0]
|
||||
|
||||
|
||||
compiled_models_array = []
|
||||
compiled_models = {}
|
||||
deviceIds = self.plugin.deviceIds
|
||||
|
||||
for deviceId in deviceIds:
|
||||
sess_options = onnxruntime.SessionOptions()
|
||||
|
||||
providers: list[str] = []
|
||||
if sys.platform == "darwin":
|
||||
providers.append("CoreMLExecutionProvider")
|
||||
|
||||
if "linux" in sys.platform and platform.machine() == "x86_64":
|
||||
deviceId = int(deviceId)
|
||||
providers.append(("CUDAExecutionProvider", {"device_id": deviceId}))
|
||||
|
||||
providers.append("CPUExecutionProvider")
|
||||
|
||||
text_model = onnxruntime.InferenceSession(
|
||||
text_onnx, sess_options=sess_options, providers=providers
|
||||
)
|
||||
vision_model = onnxruntime.InferenceSession(
|
||||
vision_onnx, sess_options=sess_options, providers=providers
|
||||
)
|
||||
compiled_models_array.append((text_model, vision_model))
|
||||
|
||||
def executor_initializer():
|
||||
thread_name = threading.current_thread().name
|
||||
interpreter = compiled_models_array.pop()
|
||||
compiled_models[thread_name] = interpreter
|
||||
print("Runtime initialized on thread {}".format(thread_name))
|
||||
|
||||
executor = concurrent.futures.ThreadPoolExecutor(
|
||||
initializer=executor_initializer,
|
||||
max_workers=len(compiled_models_array),
|
||||
thread_name_prefix="custom",
|
||||
)
|
||||
|
||||
return compiled_models, executor
|
||||
|
||||
async def detect_once(self, input: Image.Image, settings: Any, src_size, cvss):
|
||||
compiled_models, executor = self.model
|
||||
def predict():
|
||||
inputs = self.processor(images=input, return_tensors="np", padding="max_length", truncation=True)
|
||||
compiled_model = compiled_models[threading.current_thread().name]
|
||||
_, vision_session = compiled_model
|
||||
vision_predictions = vision_session.run(None, {vision_session.get_inputs()[0].name: inputs.data['pixel_values']})
|
||||
image_embeds = vision_predictions[0]
|
||||
# this is a hack to utilize the existing image massaging infrastructure
|
||||
embedding = bytearray(image_embeds.astype(np.float32).tobytes())
|
||||
ret: ObjectsDetected = {
|
||||
"detections": [
|
||||
{
|
||||
"embedding": embedding,
|
||||
}
|
||||
],
|
||||
"inputDimensions": src_size
|
||||
}
|
||||
return ret
|
||||
|
||||
objs = await asyncio.get_event_loop().run_in_executor(
|
||||
executor, predict
|
||||
)
|
||||
return objs
|
||||
|
||||
async def getTextEmbedding(self, input):
|
||||
compiled_models, executor = self.model
|
||||
def predict():
|
||||
inputs = self.processor(text=input, return_tensors="np", padding="max_length", truncation=True)
|
||||
compiled_model = compiled_models[threading.current_thread().name]
|
||||
text_session, _ = compiled_model
|
||||
text_inputs = {
|
||||
text_session.get_inputs()[0].name: inputs['input_ids'],
|
||||
text_session.get_inputs()[1].name: inputs['attention_mask']
|
||||
}
|
||||
text_predictions = text_session.run(None, text_inputs)
|
||||
text_embeds = text_predictions[0]
|
||||
return bytearray(text_embeds.astype(np.float32).tobytes())
|
||||
|
||||
objs = await asyncio.get_event_loop().run_in_executor(
|
||||
executor, predict
|
||||
)
|
||||
return objs
|
||||
127
plugins/onnx/src/ort/custom_detection.py
Normal file
127
plugins/onnx/src/ort/custom_detection.py
Normal file
@@ -0,0 +1,127 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
import onnxruntime
|
||||
import sys
|
||||
import threading
|
||||
import platform
|
||||
|
||||
from predict.custom_detect import CustomDetection
|
||||
from scrypted_sdk import ObjectsDetected
|
||||
import concurrent.futures
|
||||
|
||||
|
||||
class ONNXCustomDetection(CustomDetection):
|
||||
def __init__(self, plugin, nativeId: str):
|
||||
super().__init__(plugin=plugin, nativeId=nativeId)
|
||||
self.prefer_relu = True
|
||||
self.detectExecutor = concurrent.futures.ThreadPoolExecutor(1, "detect-custom")
|
||||
|
||||
def loadModel(self, files: list[str]):
|
||||
# find the xml file in the files list
|
||||
onnx_files = [f for f in files if f.lower().endswith('.onnx')]
|
||||
if not onnx_files:
|
||||
raise ValueError("No Manifest.json file found in the provided files list")
|
||||
onnx_file = onnx_files[0]
|
||||
|
||||
compiled_models_array = []
|
||||
compiled_models = {}
|
||||
deviceIds = self.plugin.deviceIds
|
||||
|
||||
for deviceId in deviceIds:
|
||||
sess_options = onnxruntime.SessionOptions()
|
||||
|
||||
providers: list[str] = []
|
||||
if sys.platform == "darwin":
|
||||
providers.append("CoreMLExecutionProvider")
|
||||
|
||||
if "linux" in sys.platform and platform.machine() == "x86_64":
|
||||
deviceId = int(deviceId)
|
||||
providers.append(("CUDAExecutionProvider", {"device_id": deviceId}))
|
||||
|
||||
providers.append("CPUExecutionProvider")
|
||||
|
||||
compiled_model = onnxruntime.InferenceSession(
|
||||
onnx_file, sess_options=sess_options, providers=providers
|
||||
)
|
||||
compiled_models_array.append(compiled_model)
|
||||
|
||||
input = compiled_model.get_inputs()[0]
|
||||
input_name = input.name
|
||||
|
||||
def executor_initializer():
|
||||
thread_name = threading.current_thread().name
|
||||
interpreter = compiled_models_array.pop()
|
||||
compiled_models[thread_name] = interpreter
|
||||
print("Runtime initialized on thread {}".format(thread_name))
|
||||
|
||||
executor = concurrent.futures.ThreadPoolExecutor(
|
||||
initializer=executor_initializer,
|
||||
max_workers=len(compiled_models_array),
|
||||
thread_name_prefix="custom",
|
||||
)
|
||||
|
||||
prepareExecutor = concurrent.futures.ThreadPoolExecutor(
|
||||
max_workers=len(compiled_models_array),
|
||||
thread_name_prefix="custom-prepare",
|
||||
)
|
||||
|
||||
return compiled_models, input_name, prepareExecutor, executor
|
||||
|
||||
|
||||
async def predictModel(self, input: Image.Image) -> ObjectsDetected:
|
||||
compiled_models, input_name, prepareExecutor, executor = self.model
|
||||
def predict():
|
||||
if self.model_config.get("mean", None) and self.model_config.get("std", None):
|
||||
im = np.expand_dims(input, axis=0)
|
||||
im = im.transpose((0, 3, 1, 2)) # BHWC to BCHW, (n, 3, h, w)
|
||||
im = im.astype(np.float32) / 255.0
|
||||
|
||||
mean = np.array(self.model_config["mean"])
|
||||
std = np.array(self.model_config["std"])
|
||||
mean = mean.reshape(1, -1, 1, 1)
|
||||
std = std.reshape(1, -1, 1, 1)
|
||||
im = (im - mean) / std
|
||||
|
||||
im = np.ascontiguousarray(im.astype(np.float32)) # contiguous
|
||||
|
||||
out_dict = model.predict({inputName: im})
|
||||
else:
|
||||
out_dict = self.model.predict({self.inputName: input})
|
||||
|
||||
results = list(out_dict.values())[0][0]
|
||||
return results
|
||||
|
||||
def prepare():
|
||||
im = np.array(input)
|
||||
im = np.expand_dims(input, axis=0)
|
||||
im = im.transpose((0, 3, 1, 2)) # BHWC to BCHW, (n, 3, h, w)
|
||||
im = im.astype(np.float32) / 255.0
|
||||
|
||||
if self.model_config.get("mean", None) and self.model_config.get("std", None):
|
||||
mean = np.array(self.model_config["mean"])
|
||||
std = np.array(self.model_config["std"])
|
||||
mean = mean.reshape(1, -1, 1, 1)
|
||||
std = std.reshape(1, -1, 1, 1)
|
||||
im = (im - mean) / std
|
||||
im = im.astype(np.float32)
|
||||
|
||||
im = np.ascontiguousarray(im)
|
||||
return im
|
||||
|
||||
def predict(input_tensor):
|
||||
compiled_model = compiled_models[threading.current_thread().name]
|
||||
output_tensors = compiled_model.run(None, {input_name: input_tensor})
|
||||
return output_tensors
|
||||
|
||||
input_tensor = await asyncio.get_event_loop().run_in_executor(
|
||||
prepareExecutor, lambda: prepare()
|
||||
)
|
||||
objs = await asyncio.get_event_loop().run_in_executor(
|
||||
executor, lambda: predict(input_tensor)
|
||||
)
|
||||
|
||||
return objs[0][0]
|
||||
@@ -1,7 +1,7 @@
|
||||
# uncomment to require cuda 12, but most stuff is still targetting cuda 11.
|
||||
# however, stuff targetted for cuda 11 can still run on cuda 12.
|
||||
# --extra-index-url https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/
|
||||
onnxruntime-gpu==1.19.2; 'darwin' not in sys_platform and platform_machine != 'aarch64'
|
||||
onnxruntime-gpu==1.22.0; 'darwin' not in sys_platform and platform_machine != 'aarch64'
|
||||
# cpu and coreml execution provider
|
||||
onnxruntime; 'darwin' in sys_platform or platform_machine == 'aarch64'
|
||||
# nightly?
|
||||
@@ -9,3 +9,5 @@ onnxruntime; 'darwin' in sys_platform or platform_machine == 'aarch64'
|
||||
|
||||
Pillow==10.3.0
|
||||
opencv-python-headless==4.10.0.84
|
||||
|
||||
transformers==4.52.4
|
||||
|
||||
84
plugins/openvino/package-lock.json
generated
84
plugins/openvino/package-lock.json
generated
@@ -1,36 +1,43 @@
|
||||
{
|
||||
"name": "@scrypted/openvino",
|
||||
"version": "0.1.177",
|
||||
"version": "0.1.185",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/openvino",
|
||||
"version": "0.1.177",
|
||||
"version": "0.1.185",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.29",
|
||||
"version": "0.5.20",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^1.6.5",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@rollup/plugin-commonjs": "^28.0.5",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.10.0",
|
||||
"babel-loader": "^10.0.0",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"openai": "^5.3.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.43.0",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.8.3",
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted-changelog": "bin/scrypted-changelog.js",
|
||||
@@ -42,11 +49,9 @@
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"stringify-object": "^3.3.0",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.23.21"
|
||||
"@types/node": "^24.0.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.28.5"
|
||||
}
|
||||
},
|
||||
"../sdk": {
|
||||
@@ -61,25 +66,30 @@
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^1.6.5",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@rollup/plugin-commonjs": "^28.0.5",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"@types/node": "^24.0.1",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.10.0",
|
||||
"babel-loader": "^10.0.0",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"openai": "^5.3.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"stringify-object": "^3.3.0",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.23.21",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.43.0",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typedoc": "^0.28.5",
|
||||
"typescript": "^5.8.3",
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
"runtime": "python",
|
||||
"type": "API",
|
||||
"interfaces": [
|
||||
"ScryptedSystemDevice",
|
||||
"DeviceCreator",
|
||||
"DeviceProvider",
|
||||
"Settings",
|
||||
"ClusterForkInterface",
|
||||
@@ -48,5 +50,5 @@
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.1.177"
|
||||
"version": "0.1.185"
|
||||
}
|
||||
|
||||
25
plugins/openvino/src/common/path_tools.py
Normal file
25
plugins/openvino/src/common/path_tools.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
def replace_last_path_component(url, new_path):
|
||||
# Parse the original URL
|
||||
parsed_url = urlparse(url)
|
||||
|
||||
# Split the path into components
|
||||
path_components = parsed_url.path.split('/')
|
||||
|
||||
# Remove the last component
|
||||
if len(path_components) > 1:
|
||||
path_components.pop()
|
||||
else:
|
||||
raise ValueError("URL path has no components to replace")
|
||||
|
||||
# Join the path components back together
|
||||
new_path = '/'.join(path_components) + '/' + new_path
|
||||
|
||||
# Create a new parsed URL with the updated path
|
||||
new_parsed_url = parsed_url._replace(path=new_path)
|
||||
|
||||
# Reconstruct the URL
|
||||
new_url = urlunparse(new_parsed_url)
|
||||
|
||||
return new_url
|
||||
@@ -18,6 +18,7 @@ import common.yolo as yolo
|
||||
from predict import Prediction, PredictPlugin
|
||||
from predict.rectangle import Rectangle
|
||||
|
||||
from .custom_detection import OpenVINOCustomDetection
|
||||
from .face_recognition import OpenVINOFaceRecognition
|
||||
|
||||
try:
|
||||
@@ -25,6 +26,8 @@ try:
|
||||
except:
|
||||
OpenVINOTextRecognition = None
|
||||
|
||||
from .clip_embedding import OpenVINOClipEmbedding
|
||||
|
||||
predictExecutor = concurrent.futures.ThreadPoolExecutor(
|
||||
thread_name_prefix="OpenVINO-Predict"
|
||||
)
|
||||
@@ -104,6 +107,8 @@ class OpenVINOPlugin(
|
||||
def __init__(self, nativeId: str | None = None, forked: bool = False):
|
||||
super().__init__(nativeId=nativeId, forked=forked)
|
||||
|
||||
self.custom_models = {}
|
||||
|
||||
self.core = ov.Core()
|
||||
dump_device_properties(self.core)
|
||||
available_devices = self.core.available_devices
|
||||
@@ -297,6 +302,7 @@ class OpenVINOPlugin(
|
||||
|
||||
self.faceDevice = None
|
||||
self.textDevice = None
|
||||
self.clipDevice = None
|
||||
|
||||
if not self.forked:
|
||||
asyncio.ensure_future(self.prepareRecognitionModels(), loop=self.loop)
|
||||
@@ -395,7 +401,7 @@ class OpenVINOPlugin(
|
||||
|
||||
async def prepareRecognitionModels(self):
|
||||
try:
|
||||
devices = [
|
||||
await scrypted_sdk.deviceManager.onDeviceDiscovered(
|
||||
{
|
||||
"nativeId": "facerecognition",
|
||||
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
|
||||
@@ -405,10 +411,10 @@ class OpenVINOPlugin(
|
||||
],
|
||||
"name": "OpenVINO Face Recognition",
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
if OpenVINOTextRecognition:
|
||||
devices.append(
|
||||
await scrypted_sdk.deviceManager.onDeviceDiscovered(
|
||||
{
|
||||
"nativeId": "textrecognition",
|
||||
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
|
||||
@@ -417,12 +423,20 @@ class OpenVINOPlugin(
|
||||
scrypted_sdk.ScryptedInterface.ObjectDetection.value,
|
||||
],
|
||||
"name": "OpenVINO Text Recognition",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
await scrypted_sdk.deviceManager.onDevicesChanged(
|
||||
await scrypted_sdk.deviceManager.onDeviceDiscovered(
|
||||
{
|
||||
"devices": devices,
|
||||
"nativeId": "clipembedding",
|
||||
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
|
||||
"interfaces": [
|
||||
scrypted_sdk.ScryptedInterface.ClusterForkInterface.value,
|
||||
scrypted_sdk.ScryptedInterface.ObjectDetection.value,
|
||||
scrypted_sdk.ScryptedInterface.TextEmbedding.value,
|
||||
scrypted_sdk.ScryptedInterface.ImageEmbedding.value,
|
||||
],
|
||||
"name": "OpenVINO CLIP Embedding",
|
||||
}
|
||||
)
|
||||
except:
|
||||
@@ -435,4 +449,13 @@ class OpenVINOPlugin(
|
||||
elif nativeId == "textrecognition":
|
||||
self.textDevice = self.textDevice or OpenVINOTextRecognition(self, nativeId)
|
||||
return self.textDevice
|
||||
raise Exception("unknown device")
|
||||
elif nativeId == "clipembedding":
|
||||
self.clipDevice = self.clipDevice or OpenVINOClipEmbedding(self, nativeId)
|
||||
return self.clipDevice
|
||||
custom_model = self.custom_models.get(nativeId, None)
|
||||
if custom_model:
|
||||
return custom_model
|
||||
custom_model = OpenVINOCustomDetection(self, nativeId)
|
||||
self.custom_models[nativeId] = custom_model
|
||||
await custom_model.reportDevice(nativeId, custom_model.providedName)
|
||||
return custom_model
|
||||
|
||||
87
plugins/openvino/src/ov/clip_embedding.py
Normal file
87
plugins/openvino/src/ov/clip_embedding.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import openvino.runtime as ov
|
||||
from PIL import Image
|
||||
|
||||
from ov import async_infer
|
||||
from predict.clip import ClipEmbedding
|
||||
from scrypted_sdk import ObjectsDetected
|
||||
|
||||
clipPrepare, clipPredict = async_infer.create_executors("ClipPredict")
|
||||
|
||||
|
||||
# _int8 is available but seems slower in addition to the accuracy loss.
|
||||
model_suffix = ""
|
||||
text_xml_name = f"text{model_suffix}.xml"
|
||||
vision_xml_name = f"vision{model_suffix}.xml"
|
||||
|
||||
class OpenVINOClipEmbedding(ClipEmbedding):
|
||||
def __init__(self, plugin, nativeId: str):
|
||||
super().__init__(plugin=plugin, nativeId=nativeId)
|
||||
|
||||
def getFiles(self):
|
||||
return [
|
||||
f"openvino/{text_xml_name}",
|
||||
f"openvino/text{model_suffix}.bin",
|
||||
f"openvino/{vision_xml_name}",
|
||||
f"openvino/vision{model_suffix}.bin"
|
||||
]
|
||||
|
||||
def loadModel(self, files):
|
||||
# find the xml file in the files list
|
||||
text_xml = [f for f in files if f.lower().endswith(text_xml_name)]
|
||||
if not text_xml:
|
||||
raise ValueError("No XML model file found in the provided files list")
|
||||
text_xml = text_xml[0]
|
||||
|
||||
vision_xml = [f for f in files if f.lower().endswith(vision_xml_name)]
|
||||
if not vision_xml:
|
||||
raise ValueError("No XML model file found in the provided files list")
|
||||
vision_xml = vision_xml[0]
|
||||
|
||||
textModel = self.plugin.core.compile_model(text_xml, self.plugin.mode)
|
||||
model = self.plugin.core.read_model(vision_xml)
|
||||
# for some reason this is exporting as dynamic axes and causing npu to crash
|
||||
model.reshape([1, 3, 224, 224])
|
||||
visionModel = self.plugin.core.compile_model(model, self.plugin.mode)
|
||||
return textModel, visionModel
|
||||
|
||||
async def detect_once(self, input: Image.Image, settings: Any, src_size, cvss):
|
||||
def predict():
|
||||
inputs = self.processor(images=input, return_tensors="np", padding="max_length", truncation=True)
|
||||
_, vision_model = self.model
|
||||
vision_predictions = vision_model(inputs.data['pixel_values'])
|
||||
image_embeds = vision_predictions[0]
|
||||
# this is a hack to utilize the existing image massaging infrastructure
|
||||
embedding = bytearray(image_embeds.astype(np.float32).tobytes())
|
||||
ret: ObjectsDetected = {
|
||||
"detections": [
|
||||
{
|
||||
"embedding": embedding,
|
||||
}
|
||||
],
|
||||
"inputDimensions": src_size
|
||||
}
|
||||
return ret
|
||||
|
||||
ret = await asyncio.get_event_loop().run_in_executor(
|
||||
clipPredict, lambda: predict()
|
||||
)
|
||||
return ret
|
||||
|
||||
async def getTextEmbedding(self, input):
|
||||
def predict():
|
||||
inputs = self.processor(text=input, return_tensors="np", padding="max_length", truncation=True)
|
||||
text_model, _ = self.model
|
||||
text_predictions = text_model((inputs.data['input_ids'], inputs.data['attention_mask']))
|
||||
text_embeds = text_predictions[0]
|
||||
return bytearray(text_embeds.astype(np.float32).tobytes())
|
||||
|
||||
ret = await asyncio.get_event_loop().run_in_executor(
|
||||
clipPredict, lambda: predict()
|
||||
)
|
||||
return ret
|
||||
56
plugins/openvino/src/ov/custom_detection.py
Normal file
56
plugins/openvino/src/ov/custom_detection.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
import numpy as np
|
||||
import openvino.runtime as ov
|
||||
from PIL import Image
|
||||
|
||||
from ov import async_infer
|
||||
from predict.custom_detect import CustomDetection
|
||||
from scrypted_sdk import ObjectsDetected
|
||||
|
||||
customDetectPrepare, customDetectPredict = async_infer.create_executors("CustomDetect")
|
||||
|
||||
|
||||
class OpenVINOCustomDetection(CustomDetection):
|
||||
def __init__(self, plugin, nativeId: str):
|
||||
super().__init__(plugin=plugin, nativeId=nativeId)
|
||||
self.prefer_relu = True
|
||||
|
||||
def loadModel(self, files: list[str]):
|
||||
# find the xml file in the files list
|
||||
xml_files = [f for f in files if f.lower().endswith('.xml')]
|
||||
if not xml_files:
|
||||
raise ValueError("No XML model file found in the provided files list")
|
||||
xmlFile = xml_files[0]
|
||||
|
||||
return self.plugin.core.compile_model(xmlFile, self.plugin.mode)
|
||||
|
||||
async def predictModel(self, input: Image.Image) -> ObjectsDetected:
|
||||
def predict():
|
||||
im = np.expand_dims(input, axis=0)
|
||||
im = im.transpose((0, 3, 1, 2)) # BHWC to BCHW, (n, 3, h, w)
|
||||
im = im.astype(np.float32) / 255.0
|
||||
|
||||
if self.model_config.get("mean", None) and self.model_config.get("std", None):
|
||||
mean = np.array(self.model_config["mean"]).astype(np.float32)
|
||||
std = np.array(self.model_config["std"]).astype(np.float32)
|
||||
mean = mean.reshape(1, -1, 1, 1)
|
||||
std = std.reshape(1, -1, 1, 1)
|
||||
im = (im - mean) / std
|
||||
im = im.astype(np.float32)
|
||||
|
||||
im = np.ascontiguousarray(im)
|
||||
|
||||
infer_request = self.model.create_infer_request()
|
||||
tensor = ov.Tensor(array=im)
|
||||
infer_request.set_input_tensor(tensor)
|
||||
output_tensors = infer_request.infer()
|
||||
ret = output_tensors[0][0]
|
||||
return ret
|
||||
|
||||
ret = await asyncio.get_event_loop().run_in_executor(
|
||||
customDetectPredict, lambda: predict()
|
||||
)
|
||||
return ret
|
||||
@@ -32,7 +32,9 @@ class OpenVINOTextRecognition(TextRecognition):
|
||||
model.reshape([1, 1, 64, 384])
|
||||
return self.plugin.core.compile_model(model, self.plugin.mode)
|
||||
else:
|
||||
return self.plugin.core.compile_model(xmlFile, self.plugin.mode)
|
||||
model = self.plugin.core.read_model(xmlFile)
|
||||
model.reshape([1, 3, 640, 640])
|
||||
return self.plugin.core.compile_model(model, self.plugin.mode)
|
||||
|
||||
async def predictDetectModel(self, input: np.ndarray):
|
||||
def predict():
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
import asyncio
|
||||
import math
|
||||
import os
|
||||
@@ -45,7 +48,7 @@ class Prediction:
|
||||
self.embedding = embedding
|
||||
|
||||
|
||||
class PredictPlugin(DetectPlugin, scrypted_sdk.ClusterForkInterface):
|
||||
class PredictPlugin(DetectPlugin, scrypted_sdk.ClusterForkInterface, scrypted_sdk.ScryptedSystemDevice, scrypted_sdk.DeviceCreator, scrypted_sdk.DeviceProvider):
|
||||
labels: dict
|
||||
|
||||
def __init__(
|
||||
@@ -56,6 +59,10 @@ class PredictPlugin(DetectPlugin, scrypted_sdk.ClusterForkInterface):
|
||||
):
|
||||
super().__init__(nativeId=nativeId)
|
||||
|
||||
self.systemDevice = {
|
||||
"deviceCreator": "Model",
|
||||
}
|
||||
|
||||
self.plugin = plugin
|
||||
# self.clusterIndex = 0
|
||||
|
||||
@@ -356,6 +363,10 @@ class PredictPlugin(DetectPlugin, scrypted_sdk.ClusterForkInterface):
|
||||
ret = await result.getTextRecognition()
|
||||
elif self.nativeId == "facerecognition":
|
||||
ret = await result.getFaceRecognition()
|
||||
elif self.nativeId == "clipembedding":
|
||||
ret = await result.getClipEmbedding()
|
||||
else:
|
||||
ret = await result.getCustomDetection(self.nativeId)
|
||||
return ret
|
||||
|
||||
async def startCluster(self):
|
||||
@@ -394,6 +405,79 @@ class PredictPlugin(DetectPlugin, scrypted_sdk.ClusterForkInterface):
|
||||
asyncio.ensure_future(startClusterWorker(), loop=self.loop)
|
||||
|
||||
|
||||
async def getCreateDeviceSettings(self):
|
||||
ret: list[Setting] = []
|
||||
|
||||
ret.append({
|
||||
"key": "name",
|
||||
"title": "Model Name",
|
||||
"description": "The name or description of this model. E.g., Bird Classifier."
|
||||
})
|
||||
|
||||
ret.append({
|
||||
"key": "url",
|
||||
"title": "Model URL",
|
||||
"description": "The URL of the model. This should be a Github repo or url path to the model's config.json."
|
||||
})
|
||||
|
||||
ret.append({
|
||||
"key": "info",
|
||||
"type": "html",
|
||||
"title": "Sample Model",
|
||||
"value": "<a href='https://github.com/scryptedapp/bird-classifier'>A reference bird classification model.</a>"
|
||||
})
|
||||
return ret
|
||||
|
||||
async def createDevice(self, settings):
|
||||
name = settings.get('name', None)
|
||||
if not name:
|
||||
raise Exception("Model name not provided")
|
||||
model_url: str = settings.get('url', None)
|
||||
if not model_url:
|
||||
raise Exception("Model URL not provided")
|
||||
if not model_url.endswith('config.json'):
|
||||
plugin_suffix = self.pluginId.split('/')[1]
|
||||
match = re.match(r'https://github\.com/([^/]+)/([^/]+)', model_url)
|
||||
if not match:
|
||||
raise ValueError("Invalid GitHub repository URL.")
|
||||
|
||||
org, repo = match.groups()
|
||||
model_url = f"https://raw.githubusercontent.com/{org}/{repo}/refs/heads/main/models/{plugin_suffix}/config.json"
|
||||
|
||||
response = urllib.request.urlopen(model_url)
|
||||
if response.getcode() < 200 or response.getcode() >= 300:
|
||||
raise Exception(f"non-2xx response code")
|
||||
data = response.read()
|
||||
|
||||
config = json.loads(data)
|
||||
|
||||
nativeId = ''.join(random.choices('0123456789abcdef', k=8))
|
||||
|
||||
id = await self.reportDevice(nativeId, name)
|
||||
|
||||
from .custom_detect import CustomDetection
|
||||
device: CustomDetection = await self.getDevice(nativeId)
|
||||
device.storage.setItem("config_url", model_url)
|
||||
device.storage.setItem("config", json.dumps(config))
|
||||
device.init_model()
|
||||
|
||||
return id
|
||||
|
||||
async def reportDevice(self, nativeId: str, name: str):
|
||||
return await scrypted_sdk.deviceManager.onDeviceDiscovered(
|
||||
{
|
||||
"nativeId": nativeId,
|
||||
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
|
||||
"interfaces": [
|
||||
scrypted_sdk.ScryptedInterface.ClusterForkInterface.value,
|
||||
scrypted_sdk.ScryptedInterface.ObjectDetection.value,
|
||||
scrypted_sdk.ScryptedInterface.Settings.value,
|
||||
"CustomObjectDetection",
|
||||
],
|
||||
"name": name,
|
||||
},
|
||||
)
|
||||
|
||||
class Fork:
|
||||
def __init__(self, PluginType: Any):
|
||||
if PluginType:
|
||||
@@ -409,3 +493,9 @@ class Fork:
|
||||
|
||||
async def getFaceRecognition(self):
|
||||
return await self.plugin.getDevice("facerecognition")
|
||||
|
||||
async def getClipEmbedding(self):
|
||||
return await self.plugin.getDevice("clipembedding")
|
||||
|
||||
async def getCustomDetection(self, nativeId: str):
|
||||
return await self.plugin.getDevice(nativeId)
|
||||
|
||||
63
plugins/openvino/src/predict/clip.py
Normal file
63
plugins/openvino/src/predict/clip.py
Normal file
@@ -0,0 +1,63 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import os
|
||||
from typing import Tuple
|
||||
|
||||
import scrypted_sdk
|
||||
from transformers import CLIPProcessor
|
||||
|
||||
from predict import PredictPlugin
|
||||
|
||||
|
||||
class ClipEmbedding(PredictPlugin, scrypted_sdk.TextEmbedding, scrypted_sdk.ImageEmbedding):
|
||||
def __init__(self, plugin: PredictPlugin, nativeId: str):
|
||||
super().__init__(nativeId=nativeId, plugin=plugin)
|
||||
|
||||
self.inputwidth = 224
|
||||
self.inputheight = 224
|
||||
|
||||
self.labels = {}
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.minThreshold = 0.5
|
||||
|
||||
self.model = self.initModel()
|
||||
self.processor = CLIPProcessor.from_pretrained(
|
||||
"openai/clip-vit-base-patch32",
|
||||
cache_dir=os.path.join(os.environ["SCRYPTED_PLUGIN_VOLUME"], "files", "hf"),
|
||||
)
|
||||
|
||||
def getFiles(self):
|
||||
pass
|
||||
|
||||
def initModel(self):
|
||||
local_files: list[str] = []
|
||||
for file in self.getFiles():
|
||||
remote_file = "https://huggingface.co/koushd/clip/resolve/main/" + file
|
||||
localFile = self.downloadFile(remote_file, f"{self.id}/{file}")
|
||||
local_files.append(localFile)
|
||||
return self.loadModel(local_files)
|
||||
|
||||
def loadModel(self, files: list[str]):
|
||||
pass
|
||||
|
||||
async def getImageEmbedding(self, input):
|
||||
detections = await super().detectObjects(input, None)
|
||||
return detections["detections"][0]["embedding"]
|
||||
|
||||
async def detectObjects(self, mediaObject, session = None):
|
||||
ret = await super().detectObjects(mediaObject, session)
|
||||
embedding = ret["detections"][0]['embedding']
|
||||
ret["detections"][0]['embedding'] = base64.b64encode(embedding).decode("utf-8")
|
||||
return ret
|
||||
|
||||
# width, height, channels
|
||||
def get_input_details(self) -> Tuple[int, int, int]:
|
||||
return (self.inputwidth, self.inputheight, 3)
|
||||
|
||||
def get_input_size(self) -> Tuple[float, float]:
|
||||
return (self.inputwidth, self.inputheight)
|
||||
|
||||
def get_input_format(self) -> str:
|
||||
return "rgb"
|
||||
135
plugins/openvino/src/predict/custom_detect.py
Normal file
135
plugins/openvino/src/predict/custom_detect.py
Normal file
@@ -0,0 +1,135 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any, List, Tuple
|
||||
import json
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from scrypted_sdk import (ObjectDetectionResult, ObjectDetectionSession,
|
||||
ObjectsDetected)
|
||||
import scrypted_sdk
|
||||
|
||||
from common import yolo
|
||||
from predict import PredictPlugin
|
||||
from common import softmax
|
||||
|
||||
from common.path_tools import replace_last_path_component
|
||||
|
||||
def safe_parse_json(value: str):
|
||||
try:
|
||||
return json.loads(value)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
class CustomDetection(PredictPlugin, scrypted_sdk.Settings):
|
||||
def __init__(self, plugin: PredictPlugin, nativeId: str):
|
||||
super().__init__(nativeId=nativeId, plugin=plugin)
|
||||
|
||||
if not hasattr(self, "prefer_relu"):
|
||||
self.prefer_relu = False
|
||||
|
||||
self.inputheight = 320
|
||||
self.inputwidth = 320
|
||||
|
||||
self.labels = {}
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.minThreshold = 0.5
|
||||
|
||||
self.init_model()
|
||||
|
||||
# self.detectModel = self.downloadModel("scrypted_yolov9t_relu_face_320" if self.prefer_relu else "scrypted_yolov9t_face_320")
|
||||
# self.faceModel = self.downloadModel("inception_resnet_v1")
|
||||
|
||||
def init_model(self):
|
||||
config_url = self.storage.getItem('config_url')
|
||||
if not config_url:
|
||||
return
|
||||
config_str = self.storage.getItem('config')
|
||||
if not config_str:
|
||||
return
|
||||
config = json.loads(config_str)
|
||||
self.model_config = config
|
||||
for key in self.model_config['labels']:
|
||||
self.labels[int(key)] = self.model_config['labels'][key]
|
||||
self.inputwidth = config["input_shape"][2]
|
||||
self.inputheight = config["input_shape"][3]
|
||||
files: list[str] = config["files"]
|
||||
local_files: list[str] = []
|
||||
for file in files:
|
||||
remote_file = replace_last_path_component(config_url, file)
|
||||
localFile = self.downloadFile(remote_file, f"{self.id}/{file}")
|
||||
local_files.append(localFile)
|
||||
|
||||
self.model = self.loadModel(local_files)
|
||||
|
||||
def loadModel(self, files: list[str]):
|
||||
pass
|
||||
|
||||
# width, height, channels
|
||||
def get_input_details(self) -> Tuple[int, int, int]:
|
||||
return (self.inputwidth, self.inputheight, 3)
|
||||
|
||||
def get_input_size(self) -> Tuple[float, float]:
|
||||
return (self.inputwidth, self.inputheight)
|
||||
|
||||
def get_input_format(self) -> str:
|
||||
return "rgb"
|
||||
|
||||
async def detect_once(self, input: Image.Image, settings: Any, src_size, cvss):
|
||||
results = await self.predictModel(input)
|
||||
if self.model_config["model"] == "yolov9":
|
||||
objs = yolo.parse_yolov9(results)
|
||||
ret = self.create_detection_result(objs, src_size, cvss)
|
||||
return ret
|
||||
elif self.model_config["model"] == "resnet":
|
||||
exclude_classes = safe_parse_json(self.storage.getItem('excludeClasses')) or []
|
||||
while len(exclude_classes):
|
||||
excluded_class = exclude_classes.pop()
|
||||
for idx, class_name in self.labels.items():
|
||||
if class_name == excluded_class:
|
||||
results[idx] = 0
|
||||
sm = softmax.softmax(results)
|
||||
# get anything over the threshold, sort by score, top 3
|
||||
min_indexes = np.where(sm > self.minThreshold)[0]
|
||||
min_indexes = min_indexes[np.argsort(sm[min_indexes])[::-1]]
|
||||
min_indexes = min_indexes[:3]
|
||||
detection_result: ObjectsDetected = {}
|
||||
detections: List[ObjectDetectionResult] = []
|
||||
detection_result["detections"] = detections
|
||||
detection_result["inputDimensions"] = src_size
|
||||
for idx in min_indexes:
|
||||
label = self.labels[int(idx)]
|
||||
score = float(sm[int(idx)])
|
||||
detections.append(
|
||||
{
|
||||
"className": label,
|
||||
"score": score,
|
||||
}
|
||||
)
|
||||
return detection_result
|
||||
else:
|
||||
raise ValueError("Unknown model type")
|
||||
|
||||
async def predictModel(self, input: Image.Image) -> ObjectsDetected:
|
||||
pass
|
||||
|
||||
async def getSettings(self):
|
||||
return [
|
||||
{
|
||||
'key': 'excludeClasses',
|
||||
'title': 'Exclude Classes',
|
||||
'description': 'Classes to exclude from detection.',
|
||||
'multiple': True,
|
||||
'choices': list(self.labels.values()),
|
||||
'value': safe_parse_json(self.storage.getItem('excludeClasses')),
|
||||
}
|
||||
]
|
||||
|
||||
async def putSetting(self, key: str, value: str):
|
||||
if value:
|
||||
self.storage.setItem(key, json.dumps(value))
|
||||
else:
|
||||
self.storage.removeItem(key)
|
||||
await scrypted_sdk.deviceManager.onDeviceEvent(self.nativeId, scrypted_sdk.ScryptedInterface.Settings.value, None)
|
||||
|
||||
@@ -7,3 +7,5 @@
|
||||
openvino==2024.5.0
|
||||
Pillow==10.3.0
|
||||
opencv-python-headless==4.10.0.84
|
||||
|
||||
transformers==4.52.4
|
||||
|
||||
@@ -7,7 +7,7 @@ import { OnvifCameraAPI, OnvifEvent, connectCameraAPI } from './onvif-api';
|
||||
import { listenEvents } from './onvif-events';
|
||||
import { OnvifIntercom } from './onvif-intercom';
|
||||
import { DevInfo } from './probe';
|
||||
import { AIState, Enc, ReolinkCameraClient } from './reolink-api';
|
||||
import { AIState, Enc, isDeviceNvr, ReolinkCameraClient } from './reolink-api';
|
||||
|
||||
class ReolinkCameraSiren extends ScryptedDeviceBase implements OnOff {
|
||||
sirenTimeout: NodeJS.Timeout;
|
||||
@@ -103,7 +103,6 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
|
||||
clientWithToken: ReolinkCameraClient;
|
||||
onvifClient: OnvifCameraAPI;
|
||||
onvifIntercom = new OnvifIntercom(this);
|
||||
videoStreamOptions: Promise<UrlMediaStreamOptions[]>;
|
||||
motionTimeout: NodeJS.Timeout;
|
||||
siren: ReolinkCameraSiren;
|
||||
floodlight: ReolinkCameraFloodlight;
|
||||
@@ -571,6 +570,7 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
|
||||
async listenEvents() {
|
||||
let killed = false;
|
||||
const client = this.getClient();
|
||||
const deviceInfo = await client.getDeviceInfo();
|
||||
|
||||
// reolink ai might not trigger motion if objects are detected, weird.
|
||||
const startAI = async (ret: Destroyable, triggerMotion: () => void) => {
|
||||
@@ -620,7 +620,7 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
if (!hasSucceeded)
|
||||
if (!hasSucceeded && !isDeviceNvr(deviceInfo))
|
||||
return;
|
||||
ret.emit('error', e);
|
||||
}
|
||||
@@ -769,15 +769,6 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
|
||||
}
|
||||
|
||||
async getConstructedVideoStreamOptions(): Promise<UrlMediaStreamOptions[]> {
|
||||
this.videoStreamOptions ||= this.getConstructedVideoStreamOptionsInternal().catch(e => {
|
||||
this.constructedVideoStreamOptions = undefined;
|
||||
throw e;
|
||||
});
|
||||
|
||||
return this.videoStreamOptions;
|
||||
}
|
||||
|
||||
async getConstructedVideoStreamOptionsInternal(): Promise<UrlMediaStreamOptions[]> {
|
||||
let deviceInfo: DevInfo;
|
||||
try {
|
||||
const client = this.getClient();
|
||||
|
||||
@@ -75,6 +75,8 @@ export interface PtzPreset {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const isDeviceNvr = (deviceInfo: DevInfo) => ['HOMEHUB', 'NVR', 'NVR_WIFI'].includes(deviceInfo.exactType);
|
||||
|
||||
export class ReolinkCameraClient {
|
||||
credential: AuthFetchCredentialState;
|
||||
parameters: Record<string, string>;
|
||||
@@ -320,7 +322,7 @@ export class ReolinkCameraClient {
|
||||
const deviceInfo: DevInfo = await response.body?.[0]?.value?.DevInfo;
|
||||
|
||||
// Will need to check if it's valid for NVR and NVR_WIFI
|
||||
if (!['HOMEHUB', 'NVR', 'NVR_WIFI'].includes(deviceInfo.exactType)) {
|
||||
if (!isDeviceNvr(deviceInfo)) {
|
||||
return deviceInfo;
|
||||
}
|
||||
|
||||
|
||||
2
plugins/ring/.vscode/launch.json
vendored
2
plugins/ring/.vscode/launch.json
vendored
@@ -10,7 +10,7 @@
|
||||
"port": 10081,
|
||||
"request": "attach",
|
||||
"skipFiles": [
|
||||
"**/plugin-remote-worker.*",
|
||||
"**/plugin-console.*",
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"preLaunchTask": "scrypted: deploy+debug",
|
||||
|
||||
2522
plugins/ring/package-lock.json
generated
2522
plugins/ring/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -36,7 +36,7 @@
|
||||
"dependencies": {
|
||||
"@koush/ring-client-api": "file:../../external/ring-client-api",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@scrypted/sdk": "^0.3.61",
|
||||
"@types/node": "^18.15.11",
|
||||
"axios": "^1.3.5",
|
||||
"rxjs": "^7.8.0"
|
||||
@@ -45,5 +45,5 @@
|
||||
"got": "11.8.6",
|
||||
"socket.io-client": "^2.5.0"
|
||||
},
|
||||
"version": "0.0.144"
|
||||
"version": "0.0.145"
|
||||
}
|
||||
|
||||
@@ -365,9 +365,13 @@ export abstract class RtspSmartCamera extends RtspCamera {
|
||||
return this.constructedVideoStreamOptions;
|
||||
}
|
||||
|
||||
putSettingBase(key: string, value: SettingValue): Promise<void> {
|
||||
this.constructedVideoStreamOptions = undefined;
|
||||
return super.putSettingBase(key, value);
|
||||
async putSettingBase(key: string, value: SettingValue): Promise<void> {
|
||||
try {
|
||||
return await super.putSettingBase(key, value);
|
||||
}
|
||||
finally {
|
||||
this.constructedVideoStreamOptions = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
4
plugins/tapo/package-lock.json
generated
4
plugins/tapo/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/tapo",
|
||||
"version": "0.0.20",
|
||||
"version": "0.0.22",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/tapo",
|
||||
"version": "0.0.20",
|
||||
"version": "0.0.22",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/tapo",
|
||||
"version": "0.0.20",
|
||||
"version": "0.0.22",
|
||||
"description": "Tapo Camera Plugin for Scrypted",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
|
||||
2
plugins/unifi-protect/.vscode/launch.json
vendored
2
plugins/unifi-protect/.vscode/launch.json
vendored
@@ -10,7 +10,7 @@
|
||||
"port": 10081,
|
||||
"request": "attach",
|
||||
"skipFiles": [
|
||||
"**/plugin-remote-worker.*",
|
||||
"**/plugin-console.*",
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"preLaunchTask": "scrypted: deploy+debug",
|
||||
|
||||
3803
plugins/unifi-protect/package-lock.json
generated
3803
plugins/unifi-protect/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@scrypted/unifi-protect",
|
||||
"version": "0.0.165",
|
||||
"type": "module",
|
||||
"version": "0.0.164",
|
||||
"description": "Unifi Protect Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
@@ -22,26 +23,26 @@
|
||||
"plugin"
|
||||
],
|
||||
"scrypted": {
|
||||
"rollup": true,
|
||||
"name": "Unifi Protect Plugin",
|
||||
"type": "DeviceProvider",
|
||||
"interfaces": [
|
||||
"DeviceProvider",
|
||||
"Settings"
|
||||
],
|
||||
"babel": true,
|
||||
"pluginDependencies": [
|
||||
"@scrypted/prebuffer-mixin"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.13.2",
|
||||
"@types/ws": "^8.5.14"
|
||||
"@types/node": "^22.15.29",
|
||||
"@types/ws": "^8.18.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@koush/unifi-protect": "file:../../external/unifi-protect",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"axios": "^1.7.9",
|
||||
"ws": "^8.18.0"
|
||||
"unifi-protect": "^4.21.0",
|
||||
"ws": "^8.18.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,16 +96,16 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
|
||||
}
|
||||
});
|
||||
|
||||
const camera = this.findCamera() as any;
|
||||
const camera = this.findCamera();
|
||||
|
||||
await this.protect.api.updateCamera(camera, {
|
||||
await this.protect.api.updateDevice(camera, {
|
||||
privacyZones,
|
||||
} as any);
|
||||
}
|
||||
|
||||
async ptzCommand(command: PanTiltZoomCommand): Promise<void> {
|
||||
const camera = this.findCamera() as any;
|
||||
await this.protect.api.updateCamera(camera, {
|
||||
const camera = this.findCamera();
|
||||
await this.protect.api.updateDevice(camera, {
|
||||
ispSettings: {
|
||||
zoomPosition: Math.abs(command.zoom * 100),
|
||||
}
|
||||
@@ -113,8 +113,8 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
|
||||
}
|
||||
|
||||
async setStatusLight(on: boolean) {
|
||||
const camera = this.findCamera() as any;
|
||||
await this.protect.api.updateCamera(camera, {
|
||||
const camera = this.findCamera();
|
||||
await this.protect.api.updateDevice(camera, {
|
||||
ledSettings: {
|
||||
isEnabled: on,
|
||||
}
|
||||
@@ -170,8 +170,9 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
|
||||
const ffmpegInput = JSON.parse(buffer.toString()) as FFmpegInput;
|
||||
|
||||
const camera = this.findCamera();
|
||||
const params = new URLSearchParams({ camera: camera.id });
|
||||
const response = await this.protect.loginFetch(this.protect.api.wsUrl() + "/talkback?" + params.toString());
|
||||
const endpoint = new URL(this.protect.api.getApiEndpoint("talkback"));
|
||||
endpoint.searchParams.set('camera', camera.id);
|
||||
const response = await this.protect.loginFetch(endpoint.toString());
|
||||
const tb = response.data as Record<string, string>;
|
||||
|
||||
// Adjust the URL for our address.
|
||||
@@ -275,7 +276,7 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
|
||||
classes.push('ring');
|
||||
if (this.interfaces.includes(ScryptedInterface.ObjectDetector))
|
||||
classes.push(...this.findCamera().featureFlags.smartDetectTypes);
|
||||
if ((this.findCamera().featureFlags as any as FeatureFlagsShim).hasFingerprintSensor)
|
||||
if (this.findCamera().featureFlags.hasFingerprintSensor)
|
||||
classes.push('fingerprintIdentified');
|
||||
return {
|
||||
classes,
|
||||
@@ -375,7 +376,7 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
|
||||
}
|
||||
findCamera() {
|
||||
const id = this.protect.findId(this.nativeId);
|
||||
return this.protect.api.cameras.find(camera => camera.id === id);
|
||||
return this.protect.api.bootstrap.cameras.find(camera => camera.id === id);
|
||||
}
|
||||
async getVideoStream(options?: MediaStreamOptions): Promise<MediaObject> {
|
||||
const camera = this.findCamera();
|
||||
@@ -391,7 +392,7 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
|
||||
const data = Buffer.from(JSON.stringify({
|
||||
url: u,
|
||||
container: 'rtsp',
|
||||
mediaStreamOptions: this.createMediaStreamOptions(rtspChannel, (camera as any).videoCodec),
|
||||
mediaStreamOptions: this.createMediaStreamOptions(rtspChannel, camera.videoCodec),
|
||||
} as MediaStreamUrl));
|
||||
return this.createMediaObject(data, ScryptedMimeTypes.MediaStreamUrl);
|
||||
}
|
||||
@@ -425,7 +426,7 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
|
||||
async getVideoStreamOptions(): Promise<ResponseMediaStreamOptions[]> {
|
||||
const camera = this.findCamera();
|
||||
const vsos = camera.channels
|
||||
.map(channel => this.createMediaStreamOptions(channel, (camera as any).videoCodec));
|
||||
.map(channel => this.createMediaStreamOptions(channel, camera.videoCodec));
|
||||
|
||||
return vsos;
|
||||
}
|
||||
@@ -441,7 +442,9 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
|
||||
const sanitizedBitrate = Math.min(channel.maxBitrate, Math.max(channel.minBitrate, bitrate));
|
||||
this.console.log(channel.name, 'bitrate change requested', bitrate, 'clamped to', sanitizedBitrate);
|
||||
channel.bitrate = sanitizedBitrate;
|
||||
const cameraResult = await this.protect.api.updateCameraChannels(camera);
|
||||
const cameraResult = await this.protect.api.updateDevice(camera, {
|
||||
channels: camera.channels,
|
||||
});
|
||||
if (!cameraResult) {
|
||||
throw new Error("setVideoStreamOptions failed")
|
||||
}
|
||||
@@ -458,7 +461,7 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
|
||||
|
||||
setMotionDetected(motionDetected: boolean) {
|
||||
this.motionDetected = motionDetected;
|
||||
if ((this.findCamera().featureFlags as any as FeatureFlagsShim).hasPackageCamera) {
|
||||
if (this.findCamera().featureFlags.hasPackageCamera) {
|
||||
if (deviceManager.getNativeIds().includes(this.packageCameraNativeId)) {
|
||||
this.ensurePackageCamera();
|
||||
this.packageCamera.motionDetected = motionDetected;
|
||||
@@ -467,7 +470,7 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
|
||||
}
|
||||
|
||||
setFingerprintDetected(fingerprintDetected: boolean) {
|
||||
if ((this.findCamera().featureFlags as any as FeatureFlagsShim).hasFingerprintSensor) {
|
||||
if (this.findCamera().featureFlags.hasFingerprintSensor) {
|
||||
if (deviceManager.getNativeIds().includes(this.fingerprintSensorNativeId)) {
|
||||
this.ensureFingerprintSensor();
|
||||
this.fingerprintSensor.binaryState = fingerprintDetected;
|
||||
@@ -480,7 +483,7 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
|
||||
text: title.substring(0, 30),
|
||||
type: 'CUSTOM_MESSAGE',
|
||||
};
|
||||
this.protect.api.updateCamera(this.findCamera(), {
|
||||
this.protect.api.updateDevice(this.findCamera(), {
|
||||
lcdMessage: payload,
|
||||
})
|
||||
|
||||
|
||||
@@ -14,23 +14,23 @@ export class UnifiLight extends ScryptedDeviceBase implements OnOff, Brightness,
|
||||
this.console.log(protectLight);
|
||||
}
|
||||
async turnOff(): Promise<void> {
|
||||
const result = await this.protect.api.updateLight(this.findLight(), { lightOnSettings: { isLedForceOn: false } });
|
||||
const result = await this.protect.api.updateDevice(this.findLight(), { lightOnSettings: { isLedForceOn: false } });
|
||||
if (!result)
|
||||
this.console.error('turnOff failed.');
|
||||
}
|
||||
async turnOn(): Promise<void> {
|
||||
const result = await this.protect.api.updateLight(this.findLight(), { lightOnSettings: { isLedForceOn: true } });
|
||||
const result = await this.protect.api.updateDevice(this.findLight(), { lightOnSettings: { isLedForceOn: true } });
|
||||
if (!result)
|
||||
this.console.error('turnOn failed.');
|
||||
}
|
||||
async setBrightness(brightness: number): Promise<void> {
|
||||
const ledLevel = Math.round(((brightness as number) / 20) + 1);
|
||||
this.protect.api.updateLight(this.findLight(), { lightDeviceSettings: { ledLevel } });
|
||||
this.protect.api.updateDevice(this.findLight(), { lightDeviceSettings: { ledLevel } });
|
||||
}
|
||||
|
||||
findLight() {
|
||||
const id = this.protect.findId(this.nativeId);
|
||||
return this.protect.api.lights.find(light => light.id === id);
|
||||
return this.protect.api.bootstrap.lights.find(light => light.id === id);
|
||||
}
|
||||
|
||||
updateState(light?: Readonly<ProtectLightConfig>) {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Lock, LockState, ScryptedDeviceBase } from "@scrypted/sdk";
|
||||
import { UnifiProtect } from "./main";
|
||||
import { ProtectDoorLockConfig } from "./unifi-protect";
|
||||
|
||||
export class UnifiLock extends ScryptedDeviceBase implements Lock {
|
||||
constructor(public protect: UnifiProtect, nativeId: string, protectLock: Readonly<ProtectDoorLockConfig>) {
|
||||
constructor(public protect: UnifiProtect, nativeId: string, protectLock: any) {
|
||||
super(nativeId);
|
||||
|
||||
this.updateState(protectLock);
|
||||
@@ -11,23 +10,23 @@ export class UnifiLock extends ScryptedDeviceBase implements Lock {
|
||||
}
|
||||
|
||||
async lock(): Promise<void> {
|
||||
await this.protect.loginFetch(this.protect.api.doorlocksUrl() + `/${this.findLock().id}/close`, {
|
||||
await this.protect.loginFetch(this.protect.api.getApiEndpoint('doorlocks') + `/${this.findLock().id}/close`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
async unlock(): Promise<void> {
|
||||
await this.protect.loginFetch(this.protect.api.doorlocksUrl() + `/${this.findLock().id}/open`, {
|
||||
await this.protect.loginFetch(this.protect.api.getApiEndpoint('doorlocks') + `/${this.findLock().id}/open`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
findLock() {
|
||||
const id = this.protect.findId(this.nativeId);
|
||||
return this.protect.api.doorlocks.find(doorlock => doorlock.id === id);
|
||||
return (this.protect.api.bootstrap.doorlocks as any).find(doorlock => doorlock.id === id);
|
||||
}
|
||||
|
||||
updateState(lock?: Readonly<ProtectDoorLockConfig>) {
|
||||
updateState(lock?: any) {
|
||||
lock = lock || this.findLock();
|
||||
if (!lock)
|
||||
return;
|
||||
|
||||
@@ -2,14 +2,18 @@ import { createInstanceableProviderPlugin, enableInstanceableProviderMode, isIns
|
||||
import { sleep } from "@scrypted/common/src/sleep";
|
||||
import sdk, { Device, DeviceProvider, ObjectDetectionResult, ObjectsDetected, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, Settings } from "@scrypted/sdk";
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import axios from "axios";
|
||||
import axios, {ResponseType} from "axios";
|
||||
import { UnifiCamera } from "./camera";
|
||||
import { debounceFingerprintDetected, debounceMotionDetected } from "./camera-sensors";
|
||||
import { UnifiLight } from "./light";
|
||||
import { UnifiLock } from "./lock";
|
||||
import { debounceFingerprintDetected, debounceMotionDetected } from "./camera-sensors";
|
||||
import { UnifiSensor } from "./sensor";
|
||||
import { FeatureFlagsShim, LastSeenShim } from "./shim";
|
||||
import { ProtectApi, ProtectApiUpdates, ProtectNvrUpdatePayloadCameraUpdate, ProtectNvrUpdatePayloadEventAdd } from "./unifi-protect";
|
||||
import { ProtectApi, ProtectCameraConfigInterface, ProtectEventAddInterface, ProtectEventPacket } from "./unifi-protect";
|
||||
import https from 'https';
|
||||
|
||||
const httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
const { deviceManager } = sdk;
|
||||
|
||||
@@ -46,18 +50,18 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
this.updateManagementUrl();
|
||||
}
|
||||
|
||||
handleUpdatePacket(packet: any) {
|
||||
if (packet.action.action !== "update") {
|
||||
handleUpdatePacket(packet: ProtectEventPacket) {
|
||||
if (packet.header.action !== "update") {
|
||||
return;
|
||||
}
|
||||
if (!packet.action.id) {
|
||||
if (!packet.header.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const device = this.api.cameras?.find(c => c.id === packet.action.id)
|
||||
|| this.api.lights?.find(c => c.id === packet.action.id)
|
||||
|| this.api.doorlocks?.find(c => c.id === packet.action.id)
|
||||
|| this.api.sensors?.find(c => c.id === packet.action.id);
|
||||
const device = this.api.bootstrap.cameras?.find(c => c.id === packet.header.id)
|
||||
|| this.api.bootstrap.lights?.find(c => c.id === packet.header.id)
|
||||
|| (this.api.bootstrap.doorlocks as any)?.find(c => c.id === packet.header.id)
|
||||
|| this.api.bootstrap.sensors?.find(c => c.id === packet.header.id);
|
||||
|
||||
if (!device) {
|
||||
return;
|
||||
@@ -81,27 +85,48 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
return ret;
|
||||
}
|
||||
|
||||
public async loginFetch(url: string, options?: { method?: string, signal?: AbortSignal, responseType?: axios.ResponseType }) {
|
||||
const api = this.api as any;
|
||||
if (!(await api.login()))
|
||||
throw new Error('Login failed.');
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
for (const [header, value] of api.headers) {
|
||||
headers[header] = value;
|
||||
async relogin() {
|
||||
const ip = this.getSetting('ip');
|
||||
const username = this.getSetting('username');
|
||||
const password = this.getSetting('password');
|
||||
const loginResult = await this.api.login(ip, username, password);
|
||||
if (!loginResult) {
|
||||
this.log.a('Login failed. Check credentials.');
|
||||
return;
|
||||
}
|
||||
|
||||
return axios(url, {
|
||||
responseType: options?.responseType,
|
||||
method: options?.method,
|
||||
headers,
|
||||
httpsAgent: api.httpsAgent,
|
||||
signal: options?.signal,
|
||||
})
|
||||
if (!await this.api.getBootstrap()) {
|
||||
this.reconnect('refresh failed')();
|
||||
return;
|
||||
}
|
||||
return loginResult;
|
||||
}
|
||||
|
||||
listener(event: Buffer) {
|
||||
const updatePacket = ProtectApiUpdates.decodeUpdatePacket(this.console, event);
|
||||
public async loginFetch(url: string, options?: { method?: string, signal?: AbortSignal, responseType?: ResponseType }, relogin = false) {
|
||||
try {
|
||||
const api = this.api as any;
|
||||
const headers: Record<string, string> = {};
|
||||
for (const [header, value] of api.headers) {
|
||||
headers[header] = value;
|
||||
}
|
||||
|
||||
return await axios(url, {
|
||||
responseType: options?.responseType,
|
||||
method: options?.method,
|
||||
headers,
|
||||
httpsAgent,
|
||||
signal: options?.signal,
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
if (relogin) {
|
||||
await this.relogin();
|
||||
return this.loginFetch(url, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
listener(updatePacket: ProtectEventPacket) {
|
||||
if (!updatePacket)
|
||||
return;
|
||||
|
||||
@@ -109,27 +134,27 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
|
||||
const unifiDevice = this.handleUpdatePacket(updatePacket);
|
||||
|
||||
switch (updatePacket.action.modelKey) {
|
||||
switch (updatePacket.header.modelKey) {
|
||||
case "sensor":
|
||||
case "doorlock":
|
||||
case "light":
|
||||
case "camera": {
|
||||
if (!unifiDevice) {
|
||||
this.console.log('unknown device, sync needed?', updatePacket.action.id);
|
||||
this.console.log('unknown device, sync needed?', updatePacket.header.id);
|
||||
return;
|
||||
}
|
||||
if (updatePacket.action.action !== "update") {
|
||||
unifiDevice.console.log('non update', updatePacket.action.action);
|
||||
if (updatePacket.header.action !== "update") {
|
||||
unifiDevice.console.log('non update', updatePacket.header.action);
|
||||
return;
|
||||
}
|
||||
unifiDevice.updateState();
|
||||
|
||||
if (updatePacket.action.modelKey === "doorlock")
|
||||
if (updatePacket.header.modelKey === "doorlock")
|
||||
return;
|
||||
|
||||
const payload = updatePacket.payload as any as ProtectNvrUpdatePayloadCameraUpdate & LastSeenShim;
|
||||
const payload = updatePacket.payload as ProtectCameraConfigInterface;
|
||||
|
||||
if (updatePacket.action.modelKey !== "camera")
|
||||
if (updatePacket.header.modelKey !== "camera")
|
||||
return;
|
||||
|
||||
const unifiCamera = unifiDevice as UnifiCamera;
|
||||
@@ -142,19 +167,19 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
break;
|
||||
}
|
||||
case "event": {
|
||||
if (updatePacket.action.action !== "add") {
|
||||
if ((updatePacket?.payload as any)?.end && updatePacket.action.id) {
|
||||
const payload = updatePacket.payload as ProtectEventAddInterface;
|
||||
if (updatePacket.header.action !== "add") {
|
||||
if (payload.end && updatePacket.header.id) {
|
||||
// unifi reports the event ended but it seems to take a moment before the snapshot
|
||||
// is actually ready.
|
||||
setTimeout(() => {
|
||||
const running = this.runningEvents.get(updatePacket.action.id);
|
||||
const running = this.runningEvents.get(updatePacket.header.id);
|
||||
running?.resolve?.(undefined)
|
||||
}, 2000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = updatePacket.payload as ProtectNvrUpdatePayloadEventAdd;
|
||||
if (!payload.camera)
|
||||
return;
|
||||
const nativeId = this.getNativeId({ id: payload.camera }, false);
|
||||
@@ -166,7 +191,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
}
|
||||
|
||||
const detectionId = payload.id;
|
||||
const actionId = updatePacket.action.id;
|
||||
const actionId = updatePacket.header.id;
|
||||
|
||||
let resolve: (value: unknown) => void;
|
||||
const promise = new Promise(r => resolve = r);
|
||||
@@ -203,7 +228,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
detections = payload.smartDetectTypes.map(type => ({
|
||||
className: type,
|
||||
score: payload.score,
|
||||
label: (payload as any).metadata?.[type]?.name,
|
||||
label: payload.metadata?.[type]?.name,
|
||||
}));
|
||||
}
|
||||
else {
|
||||
@@ -249,7 +274,26 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
this.console.log(message, ...parameters);
|
||||
}
|
||||
|
||||
|
||||
reconnecting = false;
|
||||
wsTimeout: NodeJS.Timeout;
|
||||
reconnect(reason: string) {
|
||||
return async () => {
|
||||
if (this.reconnecting)
|
||||
return;
|
||||
this.reconnecting = true;
|
||||
this.api?.reset();
|
||||
this.console.error('Event Listener reconnecting in 10 seconds:', reason);
|
||||
await sleep(10000);
|
||||
this.discoverDevices(0);
|
||||
}
|
||||
}
|
||||
|
||||
async discoverDevices(duration: number) {
|
||||
this.api?.reset();
|
||||
this.reconnecting = false;
|
||||
clearTimeout(this.wsTimeout);
|
||||
|
||||
const ip = this.getSetting('ip');
|
||||
const username = this.getSetting('username');
|
||||
const password = this.getSetting('password');
|
||||
@@ -271,10 +315,8 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
return
|
||||
}
|
||||
|
||||
this.api?.eventsWs?.removeAllListeners();
|
||||
this.api?.eventsWs?.close();
|
||||
if (!this.api) {
|
||||
this.api = new ProtectApi(ip, username, password, {
|
||||
this.api = new ProtectApi({
|
||||
debug() { },
|
||||
error: (...args) => {
|
||||
this.console.error(...args);
|
||||
@@ -284,48 +326,37 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
});
|
||||
}
|
||||
|
||||
let reconnecting = false;
|
||||
const reconnect = (reason: string) => {
|
||||
return async () => {
|
||||
if (reconnecting)
|
||||
return;
|
||||
reconnecting = true;
|
||||
this.api?.eventsWs?.close();
|
||||
this.api?.eventsWs?.emit('close');
|
||||
this.api?.eventsWs?.removeAllListeners();
|
||||
if (this.api.eventsWs) {
|
||||
this.console.warn('Event Listener failed to close. Requesting plugin restart.');
|
||||
deviceManager.requestRestart();
|
||||
}
|
||||
this.console.error('Event Listener reconnecting in 10 seconds:', reason);
|
||||
await sleep(10000);
|
||||
this.discoverDevices(0);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (!await this.api.refreshDevices()) {
|
||||
reconnect('refresh failed')();
|
||||
const loginResult = await this.relogin();
|
||||
if (!loginResult) {
|
||||
this.log.a('Login failed. Check credentials.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await this.api.getBootstrap()) {
|
||||
this.reconnect('refresh failed')();
|
||||
return;
|
||||
}
|
||||
|
||||
let wsTimeout: NodeJS.Timeout;
|
||||
const resetWsTimeout = () => {
|
||||
clearTimeout(wsTimeout);
|
||||
wsTimeout = setTimeout(reconnect('timeout'), 5 * 60 * 1000);
|
||||
clearTimeout(this.wsTimeout);
|
||||
this.wsTimeout = setTimeout(() => this.reconnect('timeout'), 5 * 60 * 1000);
|
||||
};
|
||||
resetWsTimeout();
|
||||
|
||||
this.api.eventsWs?.on('message', (data) => {
|
||||
this.api.on('message', message => {
|
||||
resetWsTimeout();
|
||||
this.listener(data as Buffer);
|
||||
});
|
||||
this.api.eventsWs?.on('close', reconnect('close'));
|
||||
this.api.eventsWs?.on('error', reconnect('error'));
|
||||
this.listener(message);
|
||||
})
|
||||
|
||||
const devices: Device[] = [];
|
||||
|
||||
for (let camera of this.api.cameras || []) {
|
||||
if (!this.api.bootstrap.cameras.length) {
|
||||
this.console.warn('no cameras found. is this an admin account? cancelling sync.');
|
||||
return;
|
||||
}
|
||||
|
||||
for (let camera of this.api.bootstrap.cameras || []) {
|
||||
if (camera.isAdoptedByOther) {
|
||||
this.console.log('skipping camera that is adopted by another nvr', camera.id, camera.name);
|
||||
continue;
|
||||
@@ -347,7 +378,9 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
}
|
||||
|
||||
if (needUpdate) {
|
||||
camera = await this.api.updateCameraChannels(camera);
|
||||
camera = await this.api.updateDevice(camera, {
|
||||
channels: camera.channels,
|
||||
});
|
||||
if (!camera) {
|
||||
this.log.a('Unable to enable RTSP and IDR interval on camera. Is this an admin account?');
|
||||
continue;
|
||||
@@ -392,7 +425,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
if (camera.featureFlags.hasLcdScreen) {
|
||||
d.interfaces.push(ScryptedInterface.Notifier);
|
||||
}
|
||||
if ((camera.featureFlags as any as FeatureFlagsShim).hasPackageCamera) {
|
||||
if (camera.featureFlags.hasPackageCamera) {
|
||||
d.interfaces.push(ScryptedInterface.DeviceProvider);
|
||||
}
|
||||
if (camera.featureFlags.hasLedStatus) {
|
||||
@@ -405,7 +438,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
devices.push(d);
|
||||
}
|
||||
|
||||
for (const sensor of this.api.sensors || []) {
|
||||
for (const sensor of this.api.bootstrap.sensors || []) {
|
||||
const d: Device = {
|
||||
providerNativeId: this.nativeId,
|
||||
name: sensor.name,
|
||||
@@ -433,7 +466,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
devices.push(d);
|
||||
}
|
||||
|
||||
for (const light of this.api.lights || []) {
|
||||
for (const light of this.api.bootstrap.lights || []) {
|
||||
const d: Device = {
|
||||
providerNativeId: this.nativeId,
|
||||
name: light.name,
|
||||
@@ -458,7 +491,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
devices.push(d);
|
||||
}
|
||||
|
||||
for (const lock of this.api.doorlocks || []) {
|
||||
for (const lock of (this.api.bootstrap.doorlocks as any) || []) {
|
||||
const d: Device = {
|
||||
providerNativeId: this.nativeId,
|
||||
name: lock.name,
|
||||
@@ -480,6 +513,11 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
devices.push(d);
|
||||
}
|
||||
|
||||
if (!devices.length) {
|
||||
this.console.warn('no devices found. is this an admin account? cancelling sync.');
|
||||
return;
|
||||
}
|
||||
|
||||
await deviceManager.onDevicesChanged({
|
||||
providerNativeId: this.nativeId,
|
||||
devices,
|
||||
@@ -490,12 +528,12 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
}
|
||||
|
||||
// handle package cameras as a sub device
|
||||
for (const camera of this.api.cameras) {
|
||||
for (const camera of this.api.bootstrap.cameras) {
|
||||
const devices: Device[] = [];
|
||||
|
||||
const providerNativeId = this.getNativeId(camera, true);
|
||||
|
||||
if ((camera.featureFlags as any as FeatureFlagsShim).hasPackageCamera) {
|
||||
if (camera.featureFlags.hasPackageCamera) {
|
||||
const nativeId = providerNativeId + '-packageCamera';
|
||||
const d: Device = {
|
||||
providerNativeId,
|
||||
@@ -518,7 +556,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
devices.push(d);
|
||||
}
|
||||
|
||||
if ((camera.featureFlags as any as FeatureFlagsShim).hasFingerprintSensor) {
|
||||
if (camera.featureFlags.hasFingerprintSensor) {
|
||||
const nativeId = providerNativeId + '-fingerprintSensor';
|
||||
const d: Device = {
|
||||
providerNativeId,
|
||||
@@ -569,25 +607,25 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
return this.locks.get(nativeId);
|
||||
|
||||
const id = this.findId(nativeId);
|
||||
const camera = this.api.cameras.find(camera => camera.id === id);
|
||||
const camera = this.api.bootstrap.cameras.find(camera => camera.id === id);
|
||||
if (camera) {
|
||||
const ret = new UnifiCamera(this, nativeId, camera);
|
||||
this.cameras.set(nativeId, ret);
|
||||
return ret;
|
||||
}
|
||||
const sensor = this.api.sensors.find(sensor => sensor.id === id);
|
||||
const sensor = this.api.bootstrap.sensors.find(sensor => sensor.id === id);
|
||||
if (sensor) {
|
||||
const ret = new UnifiSensor(this, nativeId, sensor);
|
||||
this.unifiSensors.set(nativeId, ret);
|
||||
return ret;
|
||||
}
|
||||
const light = this.api.lights.find(light => light.id === id);
|
||||
const light = this.api.bootstrap.lights.find(light => light.id === id);
|
||||
if (light) {
|
||||
const ret = new UnifiLight(this, nativeId, light);
|
||||
this.lights.set(nativeId, ret);
|
||||
return ret;
|
||||
}
|
||||
const lock = this.api.doorlocks?.find(lock => lock.id === id);
|
||||
const lock = (this.api.bootstrap.doorlocks as any)?.find(lock => lock.id === id);
|
||||
if (lock) {
|
||||
const ret = new UnifiLock(this, nativeId, lock);
|
||||
this.locks.set(nativeId, ret);
|
||||
@@ -636,11 +674,8 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
hide: true,
|
||||
json: true,
|
||||
defaultValue: {
|
||||
mac: {},
|
||||
anonymousDeviceId: {},
|
||||
id: {},
|
||||
nativeId: {},
|
||||
host: {},
|
||||
},
|
||||
}
|
||||
});
|
||||
@@ -655,10 +690,10 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
const idMaps = this.storageSettings.values.idMaps;
|
||||
|
||||
// try to find an existing nativeId given the mac and anonymous device id
|
||||
const found = (mac && idMaps.mac[mac])
|
||||
|| (anonymousDeviceId && idMaps.anonymousDeviceId[anonymousDeviceId])
|
||||
|| (id && idMaps.id[id])
|
||||
|| (host && idMaps.host[host])
|
||||
const found = (mac && idMaps.mac?.[mac])
|
||||
|| (anonymousDeviceId && idMaps.anonymousDeviceId?.[anonymousDeviceId])
|
||||
|| (id && idMaps.id?.[id])
|
||||
|| (host && idMaps.host?.[host])
|
||||
;
|
||||
|
||||
// use the found id if one exists (device got provisioned a new id), otherwise use the id provided by the device.
|
||||
@@ -667,25 +702,42 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
if (!update)
|
||||
return nativeId;
|
||||
|
||||
// Remove any existing mappings to this nativeId
|
||||
const cleanDict = (dict: Record<string, string>) => {
|
||||
if (!dict) return;
|
||||
const entries = Object.entries(dict);
|
||||
for (const [key, value] of entries) {
|
||||
if (value === nativeId) {
|
||||
delete dict[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean existing mappings before adding new ones
|
||||
idMaps.mac ||= {};
|
||||
idMaps.anonymousDeviceId ||= {};
|
||||
idMaps.host ||= {};
|
||||
idMaps.id ||= {};
|
||||
idMaps.nativeId ||= {};
|
||||
|
||||
cleanDict(idMaps.mac);
|
||||
cleanDict(idMaps.anonymousDeviceId);
|
||||
cleanDict(idMaps.host);
|
||||
cleanDict(idMaps.id);
|
||||
|
||||
// map the mac, host, and anonymous device id to the native id.
|
||||
if (mac) {
|
||||
idMaps.mac ||= {};
|
||||
idMaps.mac[mac] = nativeId;
|
||||
}
|
||||
if (anonymousDeviceId) {
|
||||
idMaps.anonymousDeviceId ||= {};
|
||||
idMaps.anonymousDeviceId[anonymousDeviceId] = nativeId;
|
||||
}
|
||||
if (host) {
|
||||
idMaps.host ||= {};
|
||||
idMaps.host[host] = nativeId;
|
||||
}
|
||||
|
||||
// map the id and native id to each other.
|
||||
idMaps.id ||= {};
|
||||
idMaps.id[id] = nativeId;
|
||||
|
||||
idMaps.nativeId ||= {};
|
||||
idMaps.nativeId[nativeId] = id;
|
||||
|
||||
this.storageSettings.values.idMaps = idMaps;
|
||||
|
||||
@@ -15,7 +15,7 @@ export class UnifiSensor extends ScryptedDeviceBase implements Thermometer, Humi
|
||||
|
||||
findSensor() {
|
||||
const id = this.protect.findId(this.nativeId);
|
||||
return this.protect.api.sensors.find(sensor => sensor.id === id);
|
||||
return this.protect.api.bootstrap.sensors.find(sensor => sensor.id === id);
|
||||
}
|
||||
|
||||
async setTemperatureUnit(temperatureUnit: TemperatureUnit): Promise<void> {
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
|
||||
export interface FeatureFlagsShim {
|
||||
hasPackageCamera: boolean;
|
||||
hasFingerprintSensor: boolean;
|
||||
}
|
||||
|
||||
export interface LastSeenShim {
|
||||
lastSeen: number;
|
||||
}
|
||||
|
||||
export interface PrivacyZone {
|
||||
id: number;
|
||||
name: string;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// export * from '@koush/unifi-protect'
|
||||
export * from '@koush/unifi-protect/src/index'
|
||||
export * from 'unifi-protect'
|
||||
// export * from '../../../external/unifi-protect/src/index'
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "Node16",
|
||||
"module": "es2022",
|
||||
"target": "ES2021",
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "Node16",
|
||||
"moduleResolution": "bundler",
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
"src/**/*",
|
||||
"../../common/src/**/*",
|
||||
"../../server/src/**/*"
|
||||
]
|
||||
}
|
||||
4
plugins/webrtc/package-lock.json
generated
4
plugins/webrtc/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/webrtc",
|
||||
"version": "0.2.77",
|
||||
"version": "0.2.79",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/webrtc",
|
||||
"version": "0.2.77",
|
||||
"version": "0.2.79",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/webrtc",
|
||||
"version": "0.2.78",
|
||||
"version": "0.2.79",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
|
||||
@@ -69,7 +69,7 @@ export async function createTrackForwarder(options: {
|
||||
requestDestination = 'remote';
|
||||
}
|
||||
|
||||
const hasH265Support = !!videoTransceiver.codecs.find(codec => codec.mimeType === 'video/H265');
|
||||
const hasH265Support = !!videoTransceiver?.codecs.find(codec => codec.mimeType === 'video/H265');
|
||||
|
||||
const mo = await requestMediaStream({
|
||||
video: {
|
||||
@@ -141,7 +141,7 @@ export async function createTrackForwarder(options: {
|
||||
});
|
||||
|
||||
const findAndSetCodec = (transceiver: RTCRtpTransceiver, mimeType: string) => {
|
||||
const found = transceiver.codecs.find(codec => codec.mimeType === mimeType);
|
||||
const found = transceiver?.codecs.find(codec => codec.mimeType === mimeType);
|
||||
if (found)
|
||||
transceiver.sender.codec = found;
|
||||
return found;
|
||||
|
||||
@@ -46,6 +46,8 @@ if (fs.existsSync(path.resolve(cwd, 'src/main.py'))) {
|
||||
}
|
||||
|
||||
const packageJson = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json').toString()));
|
||||
const interfaceDescriptors = packageJson.scrypted?.interfaceDescriptors;
|
||||
delete packageJson.scrypted?.interfaceDescriptors;
|
||||
|
||||
const optionalDependencies = Object.keys(packageJson.optionalDependencies || {});
|
||||
|
||||
@@ -219,6 +221,7 @@ function finishZip() {
|
||||
const sdkVersion = require(path.join(__dirname, '../package.json')).version;
|
||||
zip.addFile('sdk.json', Buffer.from(JSON.stringify({
|
||||
version: sdkVersion,
|
||||
interfaceDescriptors,
|
||||
})));
|
||||
|
||||
if (packageJson.type === 'module') {
|
||||
|
||||
1943
sdk/package-lock.json
generated
1943
sdk/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.5.12",
|
||||
"version": "0.5.33",
|
||||
"description": "",
|
||||
"main": "dist/src/index.js",
|
||||
"exports": {
|
||||
@@ -28,31 +28,32 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@rollup/plugin-commonjs": "^28.0.5",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"axios": "^1.10.0",
|
||||
"babel-loader": "^10.0.0",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"openai": "^5.3.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.27.4",
|
||||
"rollup": "^4.43.0",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-loader": "^9.5.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"typescript": "^5.8.3",
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/node": "^24.0.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.11"
|
||||
"typedoc": "^0.28.5"
|
||||
},
|
||||
"types": "dist/src/index.d.ts"
|
||||
}
|
||||
|
||||
1
sdk/polyfill/level.js
Normal file
1
sdk/polyfill/level.js
Normal file
@@ -0,0 +1 @@
|
||||
const level = __non_webpack_require__('level'); module.exports = level;
|
||||
@@ -32,6 +32,19 @@ const packageJson = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json').to
|
||||
const external = Object.keys(packageJson.optionalDependencies || {});
|
||||
const tsconfig = JSON.parse(fs.readFileSync(path.join(cwd, 'tsconfig.json').toString()));
|
||||
|
||||
external.push(
|
||||
'@scrypted/node-pty',
|
||||
'node-forge',
|
||||
'sharp',
|
||||
'level',
|
||||
'source-map-support/register',
|
||||
'adm-zip',
|
||||
"memfs",
|
||||
"realfs",
|
||||
"fakefs",
|
||||
"mdns",
|
||||
"typescript",
|
||||
);
|
||||
|
||||
const defaultMainNodeJs = 'main.nodejs.js';
|
||||
const entries = [];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from '../types/gen/index';
|
||||
import fs from 'fs';
|
||||
import type { DeviceManager, DeviceState, EndpointManager, EventListenerRegister, Logger, MediaManager, MediaObject, ScryptedInterface, ScryptedNativeId, ScryptedStatic, SystemManager, WritableDeviceState } from '../types/gen/index';
|
||||
import { DeviceBase, ScryptedInterfaceDescriptors, ScryptedInterfaceProperty, TYPES_VERSION } from '../types/gen/index';
|
||||
import { createRequire } from 'module';
|
||||
@@ -266,7 +267,23 @@ try {
|
||||
}
|
||||
|
||||
try {
|
||||
(sdk.systemManager as any).setScryptedInterfaceDescriptors?.(TYPES_VERSION, ScryptedInterfaceDescriptors)?.catch(() => { });
|
||||
let descriptors = {
|
||||
...ScryptedInterfaceDescriptors,
|
||||
};
|
||||
try {
|
||||
const sdkJson = JSON.parse(fs.readFileSync('../sdk.json').toString());
|
||||
const customDescriptors = sdkJson.interfaceDescriptors;
|
||||
if (customDescriptors) {
|
||||
descriptors = {
|
||||
...descriptors,
|
||||
...customDescriptors,
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.warn('failed to load custom interface descriptors', e);
|
||||
}
|
||||
(sdk.systemManager as any).setScryptedInterfaceDescriptors?.(TYPES_VERSION, descriptors)?.catch(() => { });
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user