@camstack/addon-model-studio 1.1.199 → 1.1.201
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD_PARTY_MODELS.md +29 -55
- package/dist/{MotionZonesSettings-JRWr-847.mjs → MotionZonesSettings-C5XCa2RN.mjs} +2 -2
- package/dist/{PrivacyMaskSettings-BfpUuDZo.mjs → PrivacyMaskSettings-Byby1Qnl.mjs} +4 -4
- package/dist/{SceneMonitorEditor-B6q_snqj.mjs → SceneMonitorEditor-BehD1iD6.mjs} +3 -3
- package/dist/_stub.js +9033 -6124
- package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-BNn0AWfL.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-BYPEJjop.mjs} +4 -4
- package/dist/_virtual_mf___mfe_internal__addon_model_studio_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CX_tb6VB.mjs +26 -0
- package/dist/addon-model-studio.css +1 -1
- package/dist/{hostInit-CnCPORCz.mjs → hostInit-Ds4oKb79.mjs} +3 -3
- package/dist/model-studio.addon.js +961 -458
- package/dist/model-studio.addon.mjs +961 -463
- package/dist/{player-overlays-RAKUtQ68.mjs → player-overlays-BdsADMdm.mjs} +2 -2
- package/dist/remoteEntry.js +1 -1
- package/dist/{responsive-Cr8pn1rO.mjs → responsive-orJ4AkQR.mjs} +1 -1
- package/dist/{scene-monitor-copy-NIVM76QJ.mjs → scene-monitor-copy-CRMa4vtd.mjs} +1 -1
- package/dist/{square-BOlfCWYp.mjs → square-CgcW-Db-.mjs} +1 -1
- package/dist/{trash-2-aAfBxjgh.mjs → trash-2-Di341FuZ.mjs} +1 -1
- package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-yi-EovNM.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-CHD2bVBg.mjs} +1 -1
- package/package.json +2 -1
- package/dist/_virtual_mf___mfe_internal__addon_model_studio_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BKQUo9FB.mjs +0 -26
|
@@ -6,33 +6,10 @@ import fs from "node:fs";
|
|
|
6
6
|
import os from "node:os";
|
|
7
7
|
import * as path$1 from "node:path";
|
|
8
8
|
import path from "node:path";
|
|
9
|
-
import { downloadFile } from "@camstack/system/addon-utils";
|
|
9
|
+
import { deleteModelFromDisk, downloadFile } from "@camstack/system/addon-utils";
|
|
10
10
|
//#region \0rolldown/runtime.js
|
|
11
11
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
12
12
|
//#endregion
|
|
13
|
-
//#region src/scrypted/scrypted-download-policy.ts
|
|
14
|
-
/**
|
|
15
|
-
* Strict per-hop allow-list for Scrypted catalog downloads.
|
|
16
|
-
*
|
|
17
|
-
* Hosts are the ones observed from the pinned revision
|
|
18
|
-
* `187402b20a3be0a7fab9bceee21d857ed6ae0064` — not a generic CDN wildcard.
|
|
19
|
-
*/
|
|
20
|
-
var SCRYPTED_DOWNLOAD_REDIRECT_HOSTS = ["huggingface.co", "us.aws.cdn.hf.co"];
|
|
21
|
-
var LOCAL_HOSTS = new Set([
|
|
22
|
-
"localhost",
|
|
23
|
-
"127.0.0.1",
|
|
24
|
-
"::1",
|
|
25
|
-
"0.0.0.0"
|
|
26
|
-
]);
|
|
27
|
-
var IPV4_RE = /^(?:\d{1,3}\.){3}\d{1,3}$/;
|
|
28
|
-
function assertScryptedDownloadUrl(url, _hop) {
|
|
29
|
-
if (url.protocol !== "https:") throw new Error(`http downgrade refused: ${url.protocol}`);
|
|
30
|
-
if (url.username || url.password) throw new Error("userinfo not allowed on download URL");
|
|
31
|
-
const host = url.hostname.toLowerCase();
|
|
32
|
-
if (LOCAL_HOSTS.has(host) || IPV4_RE.test(host) || host.includes(":")) throw new Error(`host not allowed: ${host}`);
|
|
33
|
-
if (!SCRYPTED_DOWNLOAD_REDIRECT_HOSTS.includes(host)) throw new Error(`host not allowed: ${host}`);
|
|
34
|
-
}
|
|
35
|
-
//#endregion
|
|
36
13
|
//#region src/scrypted/scrypted-yolo-manifest-data.json
|
|
37
14
|
var scrypted_yolo_manifest_data_default = /*#__PURE__*/ JSON.parse("[{\"id\":\"scrypted-yolov9t-relu-coreml\",\"label\":\"Scrypted YOLOv9T ReLU CoreML\",\"family\":\"yolov9t\",\"variant\":\"relu\",\"format\":\"coreml\",\"precision\":\"fp16\",\"files\":[{\"relativePath\":\"coreml/scrypted_yolov9t_relu/scrypted_yolov9t_relu.mlpackage/Manifest.json\",\"size\":617,\"sha256\":\"0a8733949d77e3204dba6418e81a11a8cc177015711457ce2c171266f23382d6\"},{\"relativePath\":\"coreml/scrypted_yolov9t_relu/scrypted_yolov9t_relu.mlpackage/Data/com.apple.CoreML/model.mlmodel\",\"size\":322229,\"sha256\":\"f5a62f196f1e72a84efa1637119dffb6501621af9cf6f867bc76b4b59c9242a0\"},{\"relativePath\":\"coreml/scrypted_yolov9t_relu/scrypted_yolov9t_relu.mlpackage/Data/com.apple.CoreML/weights/weight.bin\",\"size\":3797160,\"sha256\":\"b994419b8b68cfd6b89c3c6f3fe6b37458c86cc1d87fcca3df5034476a99dd38\"}],\"sizeMB\":4.12},{\"id\":\"scrypted-yolov9t-relu-onnx\",\"label\":\"Scrypted YOLOv9T ReLU ONNX\",\"family\":\"yolov9t\",\"variant\":\"relu\",\"format\":\"onnx\",\"precision\":\"fp32\",\"files\":[{\"relativePath\":\"onnx/scrypted_yolov9t_relu/scrypted_yolov9t_relu.onnx\",\"size\":8499266,\"sha256\":\"3f11ea2071b27c285b58c16636a44d6a0a27bdd1ca8b64112dfadabd39cfea47\"}],\"sizeMB\":8.5},{\"id\":\"scrypted-yolov9t-relu-test-coreml\",\"label\":\"Scrypted YOLOv9T ReLU test CoreML\",\"family\":\"yolov9t\",\"variant\":\"relu_test\",\"format\":\"coreml\",\"precision\":\"fp16\",\"files\":[{\"relativePath\":\"coreml/scrypted_yolov9t_relu_test/scrypted_yolov9t_relu_test.mlpackage/Manifest.json\",\"size\":617,\"sha256\":\"a5e0128b643f1a66f3e07d4b332f9ccbca97ccab75651c794f0b9f1fb41c8491\"},{\"relativePath\":\"coreml/scrypted_yolov9t_relu_test/scrypted_yolov9t_relu_test.mlpackage/Data/com.apple.CoreML/model.mlmodel\",\"size\":322229,\"sha256\":\"79f4f647f2a0c79db3b560c84251b48c7ba7cbe9c1fecc9674d1a603a389dfa3\"},{\"relativePath\":\"coreml/scrypted_yolov9t_relu_test/scrypted_yolov9t_relu_test.mlpackage/Data/com.apple.CoreML/weights/weight.bin\",\"size\":3797160,\"sha256\":\"72efc9dc6af9241b5e5ee7de24a4f55741f6425d0e279d797f0f223e3d355f84\"}],\"sizeMB\":4.12},{\"id\":\"scrypted-yolov9t-relu-test-onnx\",\"label\":\"Scrypted YOLOv9T ReLU test ONNX\",\"family\":\"yolov9t\",\"variant\":\"relu_test\",\"format\":\"onnx\",\"precision\":\"fp32\",\"files\":[{\"relativePath\":\"onnx/scrypted_yolov9t_relu_test/scrypted_yolov9t_relu_test.onnx\",\"size\":8499266,\"sha256\":\"d20aa0e6923a3af7f5e84e3a6d28fb00e8ecd592e3225f986f93bd34b634e2f8\"}],\"sizeMB\":8.5},{\"id\":\"scrypted-yolov9s-relu-coreml\",\"label\":\"Scrypted YOLOv9S ReLU CoreML\",\"family\":\"yolov9s\",\"variant\":\"relu\",\"format\":\"coreml\",\"precision\":\"fp16\",\"files\":[{\"relativePath\":\"coreml/scrypted_yolov9s_relu/scrypted_yolov9s_relu.mlpackage/Manifest.json\",\"size\":617,\"sha256\":\"3bcdb820b82f1e89266f2d11bae1604d3ee4ca6d11dc5bd5c57055ff4157e9f3\"},{\"relativePath\":\"coreml/scrypted_yolov9s_relu/scrypted_yolov9s_relu.mlpackage/Data/com.apple.CoreML/model.mlmodel\",\"size\":322664,\"sha256\":\"9a9c3c9cbca406ec2f0418ec867ed6b878ec6d02c16636d30e0bbd83d2ff7c40\"},{\"relativePath\":\"coreml/scrypted_yolov9s_relu/scrypted_yolov9s_relu.mlpackage/Data/com.apple.CoreML/weights/weight.bin\",\"size\":14190120,\"sha256\":\"fd51eda58a993997268f6a2e424b536ba173207ad7974ab8bee2a8ff76d52dab\"}],\"sizeMB\":14.51},{\"id\":\"scrypted-yolov9s-relu-onnx\",\"label\":\"Scrypted YOLOv9S ReLU ONNX\",\"family\":\"yolov9s\",\"variant\":\"relu\",\"format\":\"onnx\",\"precision\":\"fp32\",\"files\":[{\"relativePath\":\"onnx/scrypted_yolov9s_relu/scrypted_yolov9s_relu.onnx\",\"size\":29287339,\"sha256\":\"ca0b5bd98d5f59eb2029c6cb5129d53af59873f62a317a028beda463bbb96399\"}],\"sizeMB\":29.29},{\"id\":\"scrypted-yolov9s-relu-test-coreml\",\"label\":\"Scrypted YOLOv9S ReLU test CoreML\",\"family\":\"yolov9s\",\"variant\":\"relu_test\",\"format\":\"coreml\",\"precision\":\"fp16\",\"files\":[{\"relativePath\":\"coreml/scrypted_yolov9s_relu_test/scrypted_yolov9s_relu_test.mlpackage/Manifest.json\",\"size\":617,\"sha256\":\"e2d24de0ce7e8573f19572029765e55abc714f36a85175ad7fcb95c001a76653\"},{\"relativePath\":\"coreml/scrypted_yolov9s_relu_test/scrypted_yolov9s_relu_test.mlpackage/Data/com.apple.CoreML/model.mlmodel\",\"size\":322664,\"sha256\":\"186b147cdfd15c5ed7a381ec3a426a22cee0b5c52d56f14efd26da06357f5cb0\"},{\"relativePath\":\"coreml/scrypted_yolov9s_relu_test/scrypted_yolov9s_relu_test.mlpackage/Data/com.apple.CoreML/weights/weight.bin\",\"size\":14190120,\"sha256\":\"e50651924c2cbffd56008a8953569e66c3de69f21807fcf7d825390ce30f21f8\"}],\"sizeMB\":14.51},{\"id\":\"scrypted-yolov9s-relu-test-onnx\",\"label\":\"Scrypted YOLOv9S ReLU test ONNX\",\"family\":\"yolov9s\",\"variant\":\"relu_test\",\"format\":\"onnx\",\"precision\":\"fp32\",\"files\":[{\"relativePath\":\"onnx/scrypted_yolov9s_relu_test/scrypted_yolov9s_relu_test.onnx\",\"size\":29287339,\"sha256\":\"115ddb83333762a0f2599421335678696454a5c3571ade5df9bac8e9cb5fb4cf\"}],\"sizeMB\":29.29},{\"id\":\"scrypted-yolov9m-relu-coreml\",\"label\":\"Scrypted YOLOv9M ReLU CoreML\",\"family\":\"yolov9m\",\"variant\":\"relu\",\"format\":\"coreml\",\"precision\":\"fp16\",\"files\":[{\"relativePath\":\"coreml/scrypted_yolov9m_relu/scrypted_yolov9m_relu.mlpackage/Manifest.json\",\"size\":617,\"sha256\":\"c548c6e1ff768b2b01bd71e230aa712a1635ae2baa30e0369557a42fc531ecbf\"},{\"relativePath\":\"coreml/scrypted_yolov9m_relu/scrypted_yolov9m_relu.mlpackage/Data/com.apple.CoreML/model.mlmodel\",\"size\":247061,\"sha256\":\"1c40e02fc2a2559cb906ea6cd9da9c730d4a78f5019290cbb38d2fcaae379fe7\"},{\"relativePath\":\"coreml/scrypted_yolov9m_relu/scrypted_yolov9m_relu.mlpackage/Data/com.apple.CoreML/weights/weight.bin\",\"size\":39880616,\"sha256\":\"4cf938e5bfcc3ee1644813e4eb4702a7de450cc0581df94c719cf43d0ed2e408\"}],\"sizeMB\":40.13},{\"id\":\"scrypted-yolov9m-relu-onnx\",\"label\":\"Scrypted YOLOv9M ReLU ONNX\",\"family\":\"yolov9m\",\"variant\":\"relu\",\"format\":\"onnx\",\"precision\":\"fp32\",\"files\":[{\"relativePath\":\"onnx/scrypted_yolov9m_relu/scrypted_yolov9m_relu.onnx\",\"size\":80380404,\"sha256\":\"e7a81fb33224a0105d2a0a594c3fa43c398c361de623f1d14156ae826e7312d5\"}],\"sizeMB\":80.38},{\"id\":\"scrypted-yolov9m-relu-test-coreml\",\"label\":\"Scrypted YOLOv9M ReLU test CoreML\",\"family\":\"yolov9m\",\"variant\":\"relu_test\",\"format\":\"coreml\",\"precision\":\"fp16\",\"files\":[{\"relativePath\":\"coreml/scrypted_yolov9m_relu_test/scrypted_yolov9m_relu_test.mlpackage/Manifest.json\",\"size\":617,\"sha256\":\"8e2684b286d02b4bdf9db1027c84cb89bf8e4830cfb58269f3ec457c5a010650\"},{\"relativePath\":\"coreml/scrypted_yolov9m_relu_test/scrypted_yolov9m_relu_test.mlpackage/Data/com.apple.CoreML/model.mlmodel\",\"size\":247061,\"sha256\":\"53d5946e74b13b15c5f8616081c9f15e8a95a5df3d5b45b830fc5a7f49561c16\"},{\"relativePath\":\"coreml/scrypted_yolov9m_relu_test/scrypted_yolov9m_relu_test.mlpackage/Data/com.apple.CoreML/weights/weight.bin\",\"size\":39880616,\"sha256\":\"69774bf0032c37707601b8cdc1e1820dbd9f8288eb5d6f3879618a5d34ff5199\"}],\"sizeMB\":40.13},{\"id\":\"scrypted-yolov9m-relu-test-onnx\",\"label\":\"Scrypted YOLOv9M ReLU test ONNX\",\"family\":\"yolov9m\",\"variant\":\"relu_test\",\"format\":\"onnx\",\"precision\":\"fp32\",\"files\":[{\"relativePath\":\"onnx/scrypted_yolov9m_relu_test/scrypted_yolov9m_relu_test.onnx\",\"size\":80380404,\"sha256\":\"8bb054f099295909ec518e2a6598d63db24d3f90edc8105a0f016903f2a205a2\"}],\"sizeMB\":80.38},{\"id\":\"scrypted-yolov9c-relu-coreml\",\"label\":\"Scrypted YOLOv9C ReLU CoreML\",\"family\":\"yolov9c\",\"variant\":\"relu\",\"format\":\"coreml\",\"precision\":\"fp16\",\"files\":[{\"relativePath\":\"coreml/scrypted_yolov9c_relu/scrypted_yolov9c_relu.mlpackage/Manifest.json\",\"size\":617,\"sha256\":\"813a306ebd69cabfd31f00434f5a961feeca7d237833f3e11805dcf4ad120ba9\"},{\"relativePath\":\"coreml/scrypted_yolov9c_relu/scrypted_yolov9c_relu.mlpackage/Data/com.apple.CoreML/model.mlmodel\",\"size\":263210,\"sha256\":\"862f082619075e470738306b0ff0a37ae70a7d08e684ca6a6dfa1971d404e0c3\"},{\"relativePath\":\"coreml/scrypted_yolov9c_relu/scrypted_yolov9c_relu.mlpackage/Data/com.apple.CoreML/weights/weight.bin\",\"size\":50489960,\"sha256\":\"ed89747e58943758e2a87221d1d404f6a4466cbc039c3c6f96e327cadc403d27\"}],\"sizeMB\":50.75},{\"id\":\"scrypted-yolov9c-relu-onnx\",\"label\":\"Scrypted YOLOv9C ReLU ONNX\",\"family\":\"yolov9c\",\"variant\":\"relu\",\"format\":\"onnx\",\"precision\":\"fp32\",\"files\":[{\"relativePath\":\"onnx/scrypted_yolov9c_relu/scrypted_yolov9c_relu.onnx\",\"size\":101631962,\"sha256\":\"b9372c4428daf4cd944c114b1e80d1a9ff0487d743e53a77216a051cfbef7116\"}],\"sizeMB\":101.63},{\"id\":\"scrypted-yolov9c-relu-test-coreml\",\"label\":\"Scrypted YOLOv9C ReLU test CoreML\",\"family\":\"yolov9c\",\"variant\":\"relu_test\",\"format\":\"coreml\",\"precision\":\"fp16\",\"files\":[{\"relativePath\":\"coreml/scrypted_yolov9c_relu_test/scrypted_yolov9c_relu_test.mlpackage/Manifest.json\",\"size\":617,\"sha256\":\"966ef82a93cb0ccf1036a8c9729c065fd372fa9590a97d0945e581cc674f7ad2\"},{\"relativePath\":\"coreml/scrypted_yolov9c_relu_test/scrypted_yolov9c_relu_test.mlpackage/Data/com.apple.CoreML/model.mlmodel\",\"size\":263210,\"sha256\":\"08718ecd3e9552d21a702bd8df60911cad0eec7b2ae382466809550f14634711\"},{\"relativePath\":\"coreml/scrypted_yolov9c_relu_test/scrypted_yolov9c_relu_test.mlpackage/Data/com.apple.CoreML/weights/weight.bin\",\"size\":50489960,\"sha256\":\"fe5936b9430ab4c87f30b148220131d6ec998b2377223d033e2fa2a00e60aada\"}],\"sizeMB\":50.75},{\"id\":\"scrypted-yolov9c-relu-test-onnx\",\"label\":\"Scrypted YOLOv9C ReLU test ONNX\",\"family\":\"yolov9c\",\"variant\":\"relu_test\",\"format\":\"onnx\",\"precision\":\"fp32\",\"files\":[{\"relativePath\":\"onnx/scrypted_yolov9c_relu_test/scrypted_yolov9c_relu_test.onnx\",\"size\":101631962,\"sha256\":\"2f4e2031e8efb4cbc871594338b2e875d78ab4af3ee0258d253273b1aac84742\"}],\"sizeMB\":101.63},{\"id\":\"scrypted-yolov9t-relu-openvino-fp32\",\"label\":\"Scrypted YOLOv9T ReLU OpenVINO FP32\",\"family\":\"yolov9t\",\"variant\":\"relu\",\"format\":\"openvino\",\"precision\":\"fp32\",\"files\":[{\"relativePath\":\"openvino/scrypted_yolov9t_relu/best-converted.xml\",\"size\":628222,\"sha256\":\"01b5a9d02a0f281e1aaf4860e1b0ed3a7f16b9d78975be8ff985ae0ee283d877\"},{\"relativePath\":\"openvino/scrypted_yolov9t_relu/best-converted.bin\",\"size\":7542204,\"sha256\":\"4fe9ce8dbdf94514fdd450cbfc907f257153e4898a0c1489a44e4912d8e32cc7\"}],\"sizeMB\":8.17},{\"id\":\"scrypted-yolov9s-relu-openvino-fp32\",\"label\":\"Scrypted YOLOv9S ReLU OpenVINO FP32\",\"family\":\"yolov9s\",\"variant\":\"relu\",\"format\":\"openvino\",\"precision\":\"fp32\",\"files\":[{\"relativePath\":\"openvino/scrypted_yolov9s_relu/best-converted.xml\",\"size\":629249,\"sha256\":\"5b4a229bf7229425698b63bd065a403e440a6151a6ddff0034867659cc4a480d\"},{\"relativePath\":\"openvino/scrypted_yolov9s_relu/best-converted.bin\",\"size\":28329724,\"sha256\":\"c030bd219208ea94082578c81f383d4ef9ec45d7150eec1c2c2ff2569cf49b35\"}],\"sizeMB\":28.96},{\"id\":\"scrypted-yolov9m-relu-openvino-fp32\",\"label\":\"Scrypted YOLOv9M ReLU OpenVINO FP32\",\"family\":\"yolov9m\",\"variant\":\"relu\",\"format\":\"openvino\",\"precision\":\"fp32\",\"files\":[{\"relativePath\":\"openvino/scrypted_yolov9m_relu/best-converted.xml\",\"size\":485439,\"sha256\":\"c66f9f1965bf70fd3635d4dfc3cb94c72c06b344c256713237d77706725b9a0c\"},{\"relativePath\":\"openvino/scrypted_yolov9m_relu/best-converted.bin\",\"size\":79717372,\"sha256\":\"dfc2d8952bbca9ad0e116168e3fd5c754e601c327d78c4f4d0d4cae12f0e1d2a\"}],\"sizeMB\":80.2},{\"id\":\"scrypted-yolov9c-relu-openvino-fp32\",\"label\":\"Scrypted YOLOv9C ReLU OpenVINO FP32\",\"family\":\"yolov9c\",\"variant\":\"relu\",\"format\":\"openvino\",\"precision\":\"fp32\",\"files\":[{\"relativePath\":\"openvino/scrypted_yolov9c_relu/best-converted.xml\",\"size\":535047,\"sha256\":\"836c1e998ebf76108ff64d554fdfa07aa72c42624f1a5448701bb409f27b8d68\"},{\"relativePath\":\"openvino/scrypted_yolov9c_relu/best-converted.bin\",\"size\":100942972,\"sha256\":\"11a1b330ac9843b349927977b3c7c7f17bb42af73a7ff102969132f27ab81814\"}],\"sizeMB\":101.48},{\"id\":\"scrypted-yolov9t-relu-openvino-int8\",\"label\":\"Scrypted YOLOv9T ReLU OpenVINO INT8\",\"family\":\"yolov9t\",\"variant\":\"relu\",\"format\":\"openvino\",\"precision\":\"int8\",\"files\":[{\"relativePath\":\"openvino/scrypted_yolov9t_relu_int8/best-converted.xml\",\"size\":1380379,\"sha256\":\"e5a76bf88ac69fc0ebc50c04b61ff91550dd9c2349af1f6d7e4cac949c640cec\"},{\"relativePath\":\"openvino/scrypted_yolov9t_relu_int8/best-converted.bin\",\"size\":1954956,\"sha256\":\"98fb5b6826d7c6697a585f69ba96754873772d9d61fbed8335aab761e8248659\"}],\"sizeMB\":3.34},{\"id\":\"scrypted-yolov9t-relu-test-openvino-int8\",\"label\":\"Scrypted YOLOv9T ReLU test OpenVINO INT8\",\"family\":\"yolov9t\",\"variant\":\"relu_test\",\"format\":\"openvino\",\"precision\":\"int8\",\"files\":[{\"relativePath\":\"openvino/scrypted_yolov9t_relu_test_int8/best-converted.xml\",\"size\":1380383,\"sha256\":\"9e4119284e923e08c9929e78fd31e2552e602a552ac538ae99bf34a50dd4d172\"},{\"relativePath\":\"openvino/scrypted_yolov9t_relu_test_int8/best-converted.bin\",\"size\":1954960,\"sha256\":\"efe5b0a2fd2f5ddef5923eeb9e592a7ef14719061bb7605c86e05b35c2c58ea9\"}],\"sizeMB\":3.34},{\"id\":\"scrypted-yolov9s-relu-openvino-int8\",\"label\":\"Scrypted YOLOv9S ReLU OpenVINO INT8\",\"family\":\"yolov9s\",\"variant\":\"relu\",\"format\":\"openvino\",\"precision\":\"int8\",\"files\":[{\"relativePath\":\"openvino/scrypted_yolov9s_relu_int8/best-converted.xml\",\"size\":1382088,\"sha256\":\"4e6c24cf24afbab21462f7e1a15bd05b1cbbe5915046000e03c2435d8110bbbf\"},{\"relativePath\":\"openvino/scrypted_yolov9s_relu_int8/best-converted.bin\",\"size\":7197308,\"sha256\":\"23fd3d666a938c41db232d9c6239baec1134716cfb4ed4ddd3d602dd20eb8ac0\"}],\"sizeMB\":8.58},{\"id\":\"scrypted-yolov9s-relu-test-openvino-int8\",\"label\":\"Scrypted YOLOv9S ReLU test OpenVINO INT8\",\"family\":\"yolov9s\",\"variant\":\"relu_test\",\"format\":\"openvino\",\"precision\":\"int8\",\"files\":[{\"relativePath\":\"openvino/scrypted_yolov9s_relu_test_int8/best-converted.xml\",\"size\":1382092,\"sha256\":\"49865bfeafe7576afce089ba6f5c3e25a9192c8b514eb96265c90fe877795641\"},{\"relativePath\":\"openvino/scrypted_yolov9s_relu_test_int8/best-converted.bin\",\"size\":7197312,\"sha256\":\"78e9d36e6b74754db2b624ea3ec0b3ff57ed6dfed59a4f2225e1a271054597de\"}],\"sizeMB\":8.58},{\"id\":\"scrypted-yolov9m-relu-openvino-int8\",\"label\":\"Scrypted YOLOv9M ReLU OpenVINO INT8\",\"family\":\"yolov9m\",\"variant\":\"relu\",\"format\":\"openvino\",\"precision\":\"int8\",\"files\":[{\"relativePath\":\"openvino/scrypted_yolov9m_relu_int8/best-converted.xml\",\"size\":1024595,\"sha256\":\"1242ab8d8c73b2e80c5dd541177ade6e102a2684114b059136acb4cad6efb034\"},{\"relativePath\":\"openvino/scrypted_yolov9m_relu_int8/best-converted.bin\",\"size\":20090516,\"sha256\":\"da8e44af8bee66478e6f359e9504bfb13ab4b76f4bd8a2f1b10cfbc22febcf6e\"}],\"sizeMB\":21.12},{\"id\":\"scrypted-yolov9m-relu-test-openvino-int8\",\"label\":\"Scrypted YOLOv9M ReLU test OpenVINO INT8\",\"family\":\"yolov9m\",\"variant\":\"relu_test\",\"format\":\"openvino\",\"precision\":\"int8\",\"files\":[{\"relativePath\":\"openvino/scrypted_yolov9m_relu_test_int8/best-converted.xml\",\"size\":1024601,\"sha256\":\"24395ae5fdbb321304c865372cd2b905cdc55abb7610520f3963702e2371decb\"},{\"relativePath\":\"openvino/scrypted_yolov9m_relu_test_int8/best-converted.bin\",\"size\":20090520,\"sha256\":\"4360fe0312067f92c49f180c7b8a10271f14d58b8909aa9b96b13af4ab9289aa\"}],\"sizeMB\":21.12},{\"id\":\"scrypted-yolov9c-relu-openvino-int8\",\"label\":\"Scrypted YOLOv9C ReLU OpenVINO INT8\",\"family\":\"yolov9c\",\"variant\":\"relu\",\"format\":\"openvino\",\"precision\":\"int8\",\"files\":[{\"relativePath\":\"openvino/scrypted_yolov9c_relu_int8/best-converted.xml\",\"size\":1092695,\"sha256\":\"a45c62940120c4301d8fff9fb3b275c29e3618f403ea68a785f2f3220cefec7a\"},{\"relativePath\":\"openvino/scrypted_yolov9c_relu_int8/best-converted.bin\",\"size\":25428280,\"sha256\":\"0643403a56178c3ed1e066c7884458d8ea6780115d662bdc38d10754d7a11c79\"}],\"sizeMB\":26.52},{\"id\":\"scrypted-yolov9c-relu-test-openvino-int8\",\"label\":\"Scrypted YOLOv9C ReLU test OpenVINO INT8\",\"family\":\"yolov9c\",\"variant\":\"relu_test\",\"format\":\"openvino\",\"precision\":\"int8\",\"files\":[{\"relativePath\":\"openvino/scrypted_yolov9c_relu_test_int8/best-converted.xml\",\"size\":1092695,\"sha256\":\"a8a1899848ebd060af2326d3c8d3c09ea83354f0bedff6cce6fa08b284ea5519\"},{\"relativePath\":\"openvino/scrypted_yolov9c_relu_test_int8/best-converted.bin\",\"size\":25428280,\"sha256\":\"2eb6bb3d0a03a9b239b6f623f9e7bced62eea61fbe4d8ff89c1b3e2e2b7978a2\"}],\"sizeMB\":26.52},{\"id\":\"scrypted-yolov9s-relu-sep-320-tflite-float16\",\"label\":\"Scrypted YOLOv9s ReLU sep 320 TFLite float16\",\"family\":\"yolov9s\",\"variant\":\"relu\",\"format\":\"tflite\",\"precision\":\"float16\",\"files\":[{\"relativePath\":\"tflite/scrypted_yolov9s_relu_sep_320/best_float16.tflite\",\"size\":14482836,\"sha256\":\"04889db3b81af361c361e41ffa99327330d37b71d680a2f889e6cdf1e94c0d8c\"}],\"sizeMB\":14.48},{\"id\":\"scrypted-yolov9s-relu-sep-320-tflite-float32\",\"label\":\"Scrypted YOLOv9s ReLU sep 320 TFLite float32\",\"family\":\"yolov9s\",\"variant\":\"relu\",\"format\":\"tflite\",\"precision\":\"float32\",\"files\":[{\"relativePath\":\"tflite/scrypted_yolov9s_relu_sep_320/best_float32.tflite\",\"size\":28777068,\"sha256\":\"f5d7d08fc1d8f2262633d2df4391638e0481740331aaa1837880a47c33001a96\"}],\"sizeMB\":28.78},{\"id\":\"scrypted-yolov9s-relu-sep-320-tflite-int8\",\"label\":\"Scrypted YOLOv9s ReLU sep 320 TFLite int8\",\"family\":\"yolov9s\",\"variant\":\"relu\",\"format\":\"tflite\",\"precision\":\"int8\",\"files\":[{\"relativePath\":\"tflite/scrypted_yolov9s_relu_sep_320/best_int8.tflite\",\"size\":7325616,\"sha256\":\"1e9866b16fe80931383bf3c94dc629fe93a37ba3dc1465ea019eb0a50bd428df\"}],\"sizeMB\":7.33},{\"id\":\"scrypted-yolov9s-relu-sep-320-tflite-integer_quant\",\"label\":\"Scrypted YOLOv9s ReLU sep 320 TFLite integer_quant\",\"family\":\"yolov9s\",\"variant\":\"relu\",\"format\":\"tflite\",\"precision\":\"integer_quant\",\"files\":[{\"relativePath\":\"tflite/scrypted_yolov9s_relu_sep_320/best_integer_quant.tflite\",\"size\":7342552,\"sha256\":\"bc1c902e9a9865637ee2403f70c6bf5c29c695d9163e58ad658feb29bb097ebc\"}],\"sizeMB\":7.34},{\"id\":\"scrypted-yolov9s-relu-sep-320-tflite-full_integer_quant\",\"label\":\"Scrypted YOLOv9s ReLU sep 320 TFLite full_integer_quant\",\"family\":\"yolov9s\",\"variant\":\"relu\",\"format\":\"tflite\",\"precision\":\"full_integer_quant\",\"files\":[{\"relativePath\":\"tflite/scrypted_yolov9s_relu_sep_320/best_full_integer_quant.tflite\",\"size\":7341704,\"sha256\":\"483ce38dbe33ee5a35f40a82f56c77629a44fa5d95efd8ade9fa97e4d2a261fd\"}],\"sizeMB\":7.34},{\"id\":\"scrypted-yolov9s-relu-sep-320-tflite-edgetpu\",\"label\":\"Scrypted YOLOv9s ReLU sep 320 TFLite edgetpu\",\"family\":\"yolov9s\",\"variant\":\"relu\",\"format\":\"tflite\",\"precision\":\"edgetpu\",\"files\":[{\"relativePath\":\"tflite/scrypted_yolov9s_relu_sep_320/best_full_integer_quant_edgetpu.tflite\",\"size\":8164480,\"sha256\":\"e1e6b074a3b9fdf19a171baf615553ad375a046aa7de2a3fef9a1950175fab22\"}],\"sizeMB\":8.16}]");
|
|
38
15
|
//#endregion
|
|
@@ -129,44 +106,55 @@ function packageDirectory(files) {
|
|
|
129
106
|
return assertScryptedRelativePath(manifest.relativePath.replace(/\/Manifest\.json$/, ""));
|
|
130
107
|
}
|
|
131
108
|
function buildModel(spec) {
|
|
109
|
+
const tier = spec.family.slice(-1);
|
|
110
|
+
const int8 = spec.precision === "int8" || spec.precision === "uint8" || spec.precision === "integer_quant" || spec.precision === "full_integer_quant" || spec.precision === "edgetpu";
|
|
132
111
|
const group = {
|
|
133
112
|
family: "yolov9",
|
|
134
|
-
tier
|
|
113
|
+
tier,
|
|
135
114
|
resolution: 320,
|
|
136
|
-
...spec.variant === "relu_test" ? { optimization: "
|
|
137
|
-
...
|
|
115
|
+
...spec.variant === "relu_test" ? { optimization: "test" } : {},
|
|
116
|
+
...int8 ? { precision: "int8" } : {}
|
|
117
|
+
};
|
|
118
|
+
const sizeLabel = {
|
|
119
|
+
t: "Tiny",
|
|
120
|
+
s: "Small",
|
|
121
|
+
m: "Medium",
|
|
122
|
+
c: "Compact"
|
|
123
|
+
};
|
|
124
|
+
const canonicalId = `scrypted-yolov9${tier}-${spec.variant}${int8 ? "-int8" : ""}-320`;
|
|
125
|
+
const nameParts = [`YOLOv9 ${sizeLabel[tier] ?? tier.toUpperCase()} @320`];
|
|
126
|
+
if (spec.variant === "relu_test") nameParts.push("(Test)");
|
|
127
|
+
if (int8) nameParts.push("INT8");
|
|
128
|
+
const name = nameParts.join(" ");
|
|
129
|
+
const description = `${name} — imported from the local Scrypted YOLO catalog.`;
|
|
130
|
+
if (spec.format === "coreml") return {
|
|
131
|
+
id: canonicalId,
|
|
132
|
+
name,
|
|
133
|
+
description,
|
|
134
|
+
formats: { coreml: {
|
|
135
|
+
url: buildScryptedResolveUrl(packageDirectory(spec.files)),
|
|
136
|
+
sizeMB: spec.sizeMB,
|
|
137
|
+
isDirectory: true,
|
|
138
|
+
files: [...MLPACKAGE_INNER_FILES],
|
|
139
|
+
runtimes: ["python"]
|
|
140
|
+
} },
|
|
141
|
+
inputSize: INPUT_SIZE,
|
|
142
|
+
labels: SCRYPTED_LABEL_DEFS,
|
|
143
|
+
preprocessMode: "resize",
|
|
144
|
+
classMap: SCRYPTED_IDENTITY_CLASS_MAP,
|
|
145
|
+
group,
|
|
146
|
+
provider: "scrypted",
|
|
147
|
+
license: SCRYPTED_YOLOV9_UNKNOWN
|
|
138
148
|
};
|
|
139
|
-
if (spec.format === "coreml") {
|
|
140
|
-
const dir = packageDirectory(spec.files);
|
|
141
|
-
return {
|
|
142
|
-
id: spec.id,
|
|
143
|
-
name: spec.label,
|
|
144
|
-
description: `${spec.label} — imported from the local Scrypted YOLO catalog.`,
|
|
145
|
-
formats: { coreml: {
|
|
146
|
-
url: buildScryptedResolveUrl(dir),
|
|
147
|
-
sizeMB: spec.sizeMB,
|
|
148
|
-
isDirectory: true,
|
|
149
|
-
files: [...MLPACKAGE_INNER_FILES],
|
|
150
|
-
runtimes: ["python"]
|
|
151
|
-
} },
|
|
152
|
-
inputSize: INPUT_SIZE,
|
|
153
|
-
labels: SCRYPTED_LABEL_DEFS,
|
|
154
|
-
preprocessMode: "resize",
|
|
155
|
-
classMap: SCRYPTED_IDENTITY_CLASS_MAP,
|
|
156
|
-
group,
|
|
157
|
-
provider: "scrypted",
|
|
158
|
-
license: SCRYPTED_YOLOV9_UNKNOWN
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
149
|
if (spec.format === "openvino") {
|
|
162
150
|
const xml = spec.files.find((file) => file.relativePath.endsWith(".xml"));
|
|
163
151
|
const bin = spec.files.find((file) => file.relativePath.endsWith(".bin"));
|
|
164
152
|
if (!xml || !bin) throw new Error(`OpenVINO entry ${spec.id} is missing xml/bin`);
|
|
165
153
|
const binName = bin.relativePath.split("/").pop() ?? "best-converted.bin";
|
|
166
154
|
return {
|
|
167
|
-
id:
|
|
168
|
-
name
|
|
169
|
-
description
|
|
155
|
+
id: canonicalId,
|
|
156
|
+
name,
|
|
157
|
+
description,
|
|
170
158
|
formats: { openvino: {
|
|
171
159
|
url: buildScryptedResolveUrl(xml.relativePath),
|
|
172
160
|
sizeMB: spec.sizeMB,
|
|
@@ -185,9 +173,9 @@ function buildModel(spec) {
|
|
|
185
173
|
const main = spec.files[0];
|
|
186
174
|
if (!main) throw new Error(`Entry ${spec.id} has no files`);
|
|
187
175
|
return {
|
|
188
|
-
id:
|
|
189
|
-
name
|
|
190
|
-
description
|
|
176
|
+
id: canonicalId,
|
|
177
|
+
name,
|
|
178
|
+
description,
|
|
191
179
|
formats: { [spec.format]: {
|
|
192
180
|
url: buildScryptedResolveUrl(main.relativePath),
|
|
193
181
|
sizeMB: spec.sizeMB,
|
|
@@ -199,7 +187,8 @@ function buildModel(spec) {
|
|
|
199
187
|
classMap: SCRYPTED_IDENTITY_CLASS_MAP,
|
|
200
188
|
group,
|
|
201
189
|
provider: "scrypted",
|
|
202
|
-
license: SCRYPTED_YOLOV9_UNKNOWN
|
|
190
|
+
license: SCRYPTED_YOLOV9_UNKNOWN,
|
|
191
|
+
...spec.format === "tflite" ? { postprocessor: "yolo-split-dfl" } : {}
|
|
203
192
|
};
|
|
204
193
|
}
|
|
205
194
|
function freezeEntry(spec) {
|
|
@@ -361,192 +350,21 @@ async function fetchScryptedRemoteTree(opts) {
|
|
|
361
350
|
//#endregion
|
|
362
351
|
//#region src/scrypted/scrypted-import.ts
|
|
363
352
|
/**
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
var SCRYPTED_IMPORT_MAX_FILE_BYTES = 256 * 1024 * 1024;
|
|
369
|
-
/** Deterministic bundle digest: sorted `path\\0sha256` lines, never the primary file alone. */
|
|
370
|
-
function scryptedBundleFingerprint(entry) {
|
|
371
|
-
const hash = createHash("sha256");
|
|
372
|
-
const lines = entry.files.map((file) => `${file.relativePath}\0${file.sha256}`).toSorted();
|
|
373
|
-
hash.update(lines.join("\n"));
|
|
374
|
-
return hash.digest("hex");
|
|
375
|
-
}
|
|
376
|
-
var LFS_POINTER_PREFIX = "version https://git-lfs.github.com/spec/v1";
|
|
377
|
-
var SIZE_TOLERANCE = .01;
|
|
378
|
-
function isLfsPointer(filePath) {
|
|
379
|
-
const fd = fs$1.openSync(filePath, "r");
|
|
380
|
-
try {
|
|
381
|
-
const buf = Buffer.alloc(128);
|
|
382
|
-
const n = fs$1.readSync(fd, buf, 0, buf.length, 0);
|
|
383
|
-
return buf.subarray(0, n).toString("utf8").startsWith(LFS_POINTER_PREFIX);
|
|
384
|
-
} finally {
|
|
385
|
-
fs$1.closeSync(fd);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
async function hashFileSha256(filePath) {
|
|
389
|
-
const hash = createHash("sha256");
|
|
390
|
-
await new Promise((resolve, reject) => {
|
|
391
|
-
fs$1.createReadStream(filePath).on("data", (chunk) => hash.update(chunk)).on("end", () => resolve()).on("error", reject);
|
|
392
|
-
});
|
|
393
|
-
return hash.digest("hex");
|
|
394
|
-
}
|
|
395
|
-
function assertInside(root, dest) {
|
|
396
|
-
const rootResolved = path$1.resolve(root);
|
|
397
|
-
const destResolved = path$1.resolve(dest);
|
|
398
|
-
const prefix = rootResolved.endsWith(path$1.sep) ? rootResolved : `${rootResolved}${path$1.sep}`;
|
|
399
|
-
if (destResolved !== rootResolved && !destResolved.startsWith(prefix)) throw new Error(`Refusing to write outside the destination root: ${dest}`);
|
|
400
|
-
return destResolved;
|
|
401
|
-
}
|
|
402
|
-
function rejectSymlink(filePath) {
|
|
403
|
-
try {
|
|
404
|
-
if (fs$1.lstatSync(filePath).isSymbolicLink()) throw new Error(`Refusing symlink at ${filePath}`);
|
|
405
|
-
} catch (err) {
|
|
406
|
-
if (err.code === "ENOENT") return;
|
|
407
|
-
throw err;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
function sizeOutOfTolerance(actual, declared) {
|
|
411
|
-
if (declared <= 0) return true;
|
|
412
|
-
return Math.abs(actual - declared) / declared > SIZE_TOLERANCE;
|
|
413
|
-
}
|
|
414
|
-
function localBasename(entry) {
|
|
415
|
-
if (entry.format === "coreml") return (entry.files.find((file) => file.relativePath.endsWith("/Manifest.json"))?.relativePath.replace(/\/Manifest\.json$/, "") ?? `${entry.id}.mlpackage`).split("/").pop() ?? `${entry.id}.mlpackage`;
|
|
416
|
-
if (entry.format === "openvino") return `${entry.id}.xml`;
|
|
417
|
-
if (entry.format === "tflite") return `${entry.id}.tflite`;
|
|
418
|
-
return entry.files[0]?.relativePath.split("/").pop() ?? `${entry.id}.onnx`;
|
|
419
|
-
}
|
|
420
|
-
function buildImportedDescriptor(entry, localName) {
|
|
421
|
-
const catalog = {
|
|
422
|
-
...entry.model,
|
|
423
|
-
classMap: SCRYPTED_IDENTITY_CLASS_MAP,
|
|
424
|
-
formats: { ...entry.model.formats }
|
|
425
|
-
};
|
|
426
|
-
const format = entry.format;
|
|
427
|
-
const previous = catalog.formats[format];
|
|
428
|
-
if (format === "coreml") catalog.formats = {
|
|
429
|
-
...catalog.formats,
|
|
430
|
-
coreml: {
|
|
431
|
-
url: `camstack-local://${entry.id}/${localName}`,
|
|
432
|
-
sizeMB: previous?.sizeMB ?? entry.files.reduce((sum, file) => sum + file.size, 0) / 1e6,
|
|
433
|
-
isDirectory: true,
|
|
434
|
-
files: previous?.files ?? [
|
|
435
|
-
"Manifest.json",
|
|
436
|
-
"Data/com.apple.CoreML/model.mlmodel",
|
|
437
|
-
"Data/com.apple.CoreML/weights/weight.bin"
|
|
438
|
-
],
|
|
439
|
-
runtimes: ["python"]
|
|
440
|
-
}
|
|
441
|
-
};
|
|
442
|
-
else if (format === "openvino") catalog.formats = {
|
|
443
|
-
...catalog.formats,
|
|
444
|
-
openvino: {
|
|
445
|
-
url: `camstack-local://${entry.id}/${localName}`,
|
|
446
|
-
sizeMB: previous?.sizeMB ?? entry.files.reduce((sum, file) => sum + file.size, 0) / 1e6,
|
|
447
|
-
files: [`${entry.id}.bin`],
|
|
448
|
-
runtimes: ["python"]
|
|
449
|
-
}
|
|
450
|
-
};
|
|
451
|
-
else catalog.formats = {
|
|
452
|
-
...catalog.formats,
|
|
453
|
-
[format]: {
|
|
454
|
-
url: `camstack-local://${entry.id}/${localName}`,
|
|
455
|
-
sizeMB: previous?.sizeMB ?? entry.files.reduce((sum, file) => sum + file.size, 0) / 1e6,
|
|
456
|
-
...previous?.runtimes !== void 0 ? { runtimes: previous.runtimes } : {}
|
|
457
|
-
}
|
|
458
|
-
};
|
|
353
|
+
* Library registration descriptor: keeps manifesto remote URLs (HF resolve).
|
|
354
|
+
* Files land on a node only via Distribute / downloadModel — not hub staging.
|
|
355
|
+
*/
|
|
356
|
+
function buildScryptedRegistryDescriptor(entry) {
|
|
459
357
|
return {
|
|
460
358
|
stepId: "object-detection",
|
|
461
|
-
entry:
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const format = entry.format;
|
|
469
|
-
if (!descriptor.entry.formats[format]) throw new Error(`resolveScryptedImportedFormat: descriptor for "${manifestId}" is missing manifesto format ${format}`);
|
|
470
|
-
return format;
|
|
471
|
-
}
|
|
472
|
-
async function verifyScryptedStagedFile(destPath, file, deps) {
|
|
473
|
-
const maxBytes = deps.maxFileBytes ?? 268435456;
|
|
474
|
-
if (file.size > maxBytes) throw new Error(`Declared size ${file.size} exceeds the ${maxBytes}-byte limit`);
|
|
475
|
-
if (!fs$1.existsSync(destPath)) throw new Error(`missing downloaded file for ${file.relativePath}`);
|
|
476
|
-
rejectSymlink(destPath);
|
|
477
|
-
const actual = fs$1.statSync(destPath).size;
|
|
478
|
-
if (actual > maxBytes) throw new Error(`Downloaded ${actual} bytes exceeds the ${maxBytes}-byte limit`);
|
|
479
|
-
if (isLfsPointer(destPath)) throw new Error(`Downloaded an LFS pointer instead of the artifact (${file.relativePath})`);
|
|
480
|
-
if (sizeOutOfTolerance(actual, file.size)) throw new Error(`Downloaded size ${actual} is outside tolerance of declared ${file.size} for ${file.relativePath}`);
|
|
481
|
-
const expected = assertScryptedFileSha256(file.sha256, file.relativePath);
|
|
482
|
-
if (await (deps.fileSha256 ?? hashFileSha256)(destPath) !== expected) throw new Error(`sha256 checksum mismatch for ${file.relativePath}`);
|
|
483
|
-
}
|
|
484
|
-
async function importScryptedCatalogModel(input, deps) {
|
|
485
|
-
const entry = (deps.resolveEntry ?? getScryptedManifestEntry)(input.manifestId);
|
|
486
|
-
if (!entry) throw new Error(`importScryptedCatalogModel: unknown manifest id "${input.manifestId}"`);
|
|
487
|
-
for (const file of entry.files) assertScryptedFileSha256(file.sha256, file.relativePath);
|
|
488
|
-
const drift = normalizeScryptedDrift(await deps.driftFor(entry.id));
|
|
489
|
-
if (drift.status !== "ok") throw new Error(`importScryptedCatalogModel: entry "${entry.id}" is blocked by drift (${drift.status}${drift.details ? `: ${drift.details}` : ""})`);
|
|
490
|
-
fs$1.mkdirSync(deps.stagingDir, { recursive: true });
|
|
491
|
-
fs$1.mkdirSync(deps.modelsDir, { recursive: true });
|
|
492
|
-
const stagingRoot = path$1.join(deps.stagingDir, `imp-${randomUUID()}`);
|
|
493
|
-
fs$1.mkdirSync(stagingRoot, { recursive: true });
|
|
494
|
-
try {
|
|
495
|
-
const staged = [];
|
|
496
|
-
for (const file of entry.files) {
|
|
497
|
-
const relative = assertScryptedRelativePath(file.relativePath);
|
|
498
|
-
const dest = assertInside(stagingRoot, path$1.join(stagingRoot, ...relative.split("/")));
|
|
499
|
-
const url = buildScryptedResolveUrl(relative);
|
|
500
|
-
await deps.downloadFile(url, dest);
|
|
501
|
-
await verifyScryptedStagedFile(dest, file, deps);
|
|
502
|
-
staged.push({
|
|
503
|
-
file,
|
|
504
|
-
dest
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
if (entry.format === "coreml") {
|
|
508
|
-
const hasManifest = staged.some((row) => row.file.relativePath.endsWith("Manifest.json"));
|
|
509
|
-
const hasModel = staged.some((row) => row.file.relativePath.endsWith("model.mlmodel"));
|
|
510
|
-
const hasWeight = staged.some((row) => row.file.relativePath.endsWith("weight.bin"));
|
|
511
|
-
if (!hasManifest || !hasModel || !hasWeight || staged.length < 3) throw new Error("CoreML payload is incomplete — Manifest.json, model and weights are required");
|
|
512
|
-
}
|
|
513
|
-
if (entry.format === "openvino") {
|
|
514
|
-
const hasXml = staged.some((row) => row.file.relativePath.endsWith(".xml"));
|
|
515
|
-
const hasBin = staged.some((row) => row.file.relativePath.endsWith(".bin"));
|
|
516
|
-
if (!hasXml || !hasBin) throw new Error("OpenVINO payload is incomplete — xml and bin siblings are required");
|
|
517
|
-
}
|
|
518
|
-
const localName = localBasename(entry);
|
|
519
|
-
const finalMain = assertInside(deps.modelsDir, path$1.join(deps.modelsDir, localName));
|
|
520
|
-
rejectSymlink(finalMain);
|
|
521
|
-
if (entry.format === "coreml") {
|
|
522
|
-
const pkgRel = staged.find((row) => row.file.relativePath.endsWith("Manifest.json"))?.file.relativePath.replace(/\/Manifest\.json$/, "");
|
|
523
|
-
if (!pkgRel) throw new Error("CoreML payload is incomplete — Manifest.json missing");
|
|
524
|
-
const stagedPkg = path$1.join(stagingRoot, ...pkgRel.split("/"));
|
|
525
|
-
fs$1.rmSync(finalMain, {
|
|
526
|
-
recursive: true,
|
|
527
|
-
force: true
|
|
528
|
-
});
|
|
529
|
-
fs$1.renameSync(stagedPkg, finalMain);
|
|
530
|
-
} else if (entry.format === "openvino") {
|
|
531
|
-
const xml = staged.find((row) => row.file.relativePath.endsWith(".xml"));
|
|
532
|
-
const bin = staged.find((row) => row.file.relativePath.endsWith(".bin"));
|
|
533
|
-
if (!xml || !bin) throw new Error("OpenVINO payload is incomplete");
|
|
534
|
-
const xmlDest = finalMain;
|
|
535
|
-
const binDest = assertInside(deps.modelsDir, path$1.join(deps.modelsDir, `${entry.id}.bin`));
|
|
536
|
-
fs$1.renameSync(xml.dest, xmlDest);
|
|
537
|
-
fs$1.renameSync(bin.dest, binDest);
|
|
538
|
-
} else {
|
|
539
|
-
const only = staged[0];
|
|
540
|
-
if (!only) throw new Error("payload is incomplete");
|
|
541
|
-
fs$1.renameSync(only.dest, finalMain);
|
|
359
|
+
entry: {
|
|
360
|
+
...entry.model,
|
|
361
|
+
classMap: SCRYPTED_IDENTITY_CLASS_MAP,
|
|
362
|
+
formats: { ...entry.model.formats },
|
|
363
|
+
name: entry.model.name,
|
|
364
|
+
description: entry.model.description,
|
|
365
|
+
provider: "scrypted"
|
|
542
366
|
}
|
|
543
|
-
|
|
544
|
-
} finally {
|
|
545
|
-
fs$1.rmSync(stagingRoot, {
|
|
546
|
-
recursive: true,
|
|
547
|
-
force: true
|
|
548
|
-
});
|
|
549
|
-
}
|
|
367
|
+
};
|
|
550
368
|
}
|
|
551
369
|
//#endregion
|
|
552
370
|
//#region ../types/dist/event-category-BGmz_oxW.mjs
|
|
@@ -11151,11 +10969,11 @@ var ModelVariantGroupSchema = object({
|
|
|
11151
10969
|
/** Quantization axis. Omit ⇒ the fp32 base build. */
|
|
11152
10970
|
precision: _enum(["fp32", "int8"]).optional(),
|
|
11153
10971
|
/**
|
|
11154
|
-
*
|
|
11155
|
-
*
|
|
11156
|
-
*
|
|
10972
|
+
* Training / latency-variant axis. Omit ⇒ the standard build. `test` marks
|
|
10973
|
+
* an alternate training of the same family (Scrypted `relu_test`); chip
|
|
10974
|
+
* label is "Test".
|
|
11157
10975
|
*/
|
|
11158
|
-
optimization: _enum(["standard", "
|
|
10976
|
+
optimization: _enum(["standard", "test"]).optional(),
|
|
11159
10977
|
/**
|
|
11160
10978
|
* Input-resolution axis (square input side, px). Omit ⇒ the family's native
|
|
11161
10979
|
* resolution (640 for yolo26). Reduced-input builds (320 / 256) are a big,
|
|
@@ -11178,9 +10996,10 @@ var ModelProviderIdSchema = _enum([
|
|
|
11178
10996
|
* are per upstream. A persisted custom-model row written when this field was a
|
|
11179
10997
|
* string still parses — the string is lifted into `{ weights }`.
|
|
11180
10998
|
*
|
|
11181
|
-
* Every licence surface (the
|
|
11182
|
-
*
|
|
11183
|
-
*
|
|
10999
|
+
* Every machine licence surface (`THIRD_PARTY_MODELS.md`, the Hugging Face
|
|
11000
|
+
* model table) is GENERATED from these records by
|
|
11001
|
+
* `scripts/gen-model-licenses.ts` (D661). Operator-facing docs live in
|
|
11002
|
+
* `camstack/docs` → `/docs/detection/models`.
|
|
11184
11003
|
*/
|
|
11185
11004
|
var ModelLicenseSchema = preprocess((value) => typeof value === "string" ? { weights: value } : value, object({
|
|
11186
11005
|
/** SPDX id of the WEIGHTS' terms, a `LicenseRef-*` for non-SPDX terms, or `UNKNOWN`. */
|
|
@@ -14743,7 +14562,7 @@ DeviceType.Camera;
|
|
|
14743
14562
|
* This matters here more than anywhere else in the repo, because every provider
|
|
14744
14563
|
* that knows a device's host knows its PASSWORD too: `{ host, port, username,
|
|
14745
14564
|
* password }` sit in one object on Hikvision, Amcrest, Reolink and ONVIF alike,
|
|
14746
|
-
* and `http://admin:hunter2@192.168.
|
|
14565
|
+
* and `http://admin:hunter2@192.168.60.139/` is a URL a browser accepts. The
|
|
14747
14566
|
* camera's own page will ask for its own login. That is correct, and pre-
|
|
14748
14567
|
* filling it is the operator's business, not ours.
|
|
14749
14568
|
*
|
|
@@ -17733,7 +17552,7 @@ var modelConvertCapability = {
|
|
|
17733
17552
|
}) }
|
|
17734
17553
|
};
|
|
17735
17554
|
method(object({
|
|
17736
|
-
|
|
17555
|
+
targetNodeId: string(),
|
|
17737
17556
|
modelId: string(),
|
|
17738
17557
|
format: _enum(MODEL_FORMATS),
|
|
17739
17558
|
entry: ModelCatalogEntrySchema
|
|
@@ -20199,8 +20018,9 @@ var NcRuleSchema = NcRuleInputSchema.extend({
|
|
|
20199
20018
|
disabledTargetIds: array(string()).default([])
|
|
20200
20019
|
});
|
|
20201
20020
|
/**
|
|
20202
|
-
* The state-summary SECTIONS (D647 / D698): the D637 kind groups that
|
|
20203
|
-
* still-true state. `
|
|
20021
|
+
* The state-summary SECTIONS (D647 / D698 / D708): the D637 kind groups that
|
|
20022
|
+
* report a still-true state. `addons` is the crash-looped addons only and
|
|
20023
|
+
* `addon-updates` the addons that are behind (D708 split them). `backups-exports` and `alarm` are not sections — a completed
|
|
20204
20024
|
* job is not an open state, and the alarm stays instant.
|
|
20205
20025
|
*/
|
|
20206
20026
|
var NcStateSummarySectionIdSchema = _enum([
|
|
@@ -20212,6 +20032,7 @@ var NcStateSummarySectionIdSchema = _enum([
|
|
|
20212
20032
|
"detection-blind",
|
|
20213
20033
|
"nodes",
|
|
20214
20034
|
"addons",
|
|
20035
|
+
"addon-updates",
|
|
20215
20036
|
"updates"
|
|
20216
20037
|
]);
|
|
20217
20038
|
/** One section of a previewed state summary. `unavailable` = its state could not be read. */
|
|
@@ -20692,7 +20513,19 @@ var NcAlarmModeDelaysListSchema = array(object({
|
|
|
20692
20513
|
* When this mode may be armed. Absent / null = always allowed (same as a
|
|
20693
20514
|
* rule with no `schedule`). Evaluated with {@link isScheduleActive}.
|
|
20694
20515
|
*/
|
|
20695
|
-
schedule: NcScheduleSchema.optional()
|
|
20516
|
+
schedule: NcScheduleSchema.optional(),
|
|
20517
|
+
/**
|
|
20518
|
+
* D706: while this mode's ENTRY DELAY runs, do the alarm rules' own pushes go out (as an
|
|
20519
|
+
* intrusion naming the locations)? Absent = false — the entry-delay notification is the
|
|
20520
|
+
* only one, and the matches ride the triggered notification's mosaic (D705).
|
|
20521
|
+
*/
|
|
20522
|
+
notifyDuringEntryDelay: boolean().optional(),
|
|
20523
|
+
/**
|
|
20524
|
+
* D706: notify "watched again" when a sensor this arm bypassed closes and opens again.
|
|
20525
|
+
* Absent = false (logged, not sent). Even when true it is not sent when that same sensor
|
|
20526
|
+
* is the one that starts the entry delay — the entry-delay notification already names it.
|
|
20527
|
+
*/
|
|
20528
|
+
notifySensorRewatched: boolean().optional()
|
|
20696
20529
|
})).max(8).refine(uniqueModes, { message: "modeDelays: at most one entry per arm mode" });
|
|
20697
20530
|
var NcAlarmModeDelaysEditListSchema = array(object({
|
|
20698
20531
|
mode: AlarmArmModeSchema,
|
|
@@ -20701,7 +20534,11 @@ var NcAlarmModeDelaysEditListSchema = array(object({
|
|
|
20701
20534
|
triggeredDurationSec: NcAlarmTriggeredSecSchema.nullable().optional(),
|
|
20702
20535
|
postTriggeredAction: NcAlarmPostTriggeredActionSchema.nullable().optional(),
|
|
20703
20536
|
/** Set a schedule, or `null` to clear (mode always allowed again). */
|
|
20704
|
-
schedule: NcScheduleSchema.nullable().optional()
|
|
20537
|
+
schedule: NcScheduleSchema.nullable().optional(),
|
|
20538
|
+
/** D706: set the switch, or `null` to clear it (back to the default, false). */
|
|
20539
|
+
notifyDuringEntryDelay: boolean().nullable().optional(),
|
|
20540
|
+
/** D706: set the switch, or `null` to clear it (back to the default, false). */
|
|
20541
|
+
notifySensorRewatched: boolean().nullable().optional()
|
|
20705
20542
|
})).max(8).refine(uniqueModes, { message: "modeDelays: at most one edit per arm mode" });
|
|
20706
20543
|
/** One switch of the alarm table: put ONE device on, or take it off, ONE mode's list. */
|
|
20707
20544
|
var NcAlarmNonBlockingEditSchema = object({
|
|
@@ -21662,39 +21499,19 @@ Object.freeze(Object.fromEntries([{
|
|
|
21662
21499
|
label: "Face recognition model",
|
|
21663
21500
|
defaultModelId: "auraface-r100",
|
|
21664
21501
|
indexName: "the enrolled face gallery",
|
|
21665
|
-
options: [
|
|
21666
|
-
|
|
21667
|
-
|
|
21668
|
-
|
|
21669
|
-
},
|
|
21670
|
-
{
|
|
21671
|
-
id: "arcface-r100",
|
|
21672
|
-
label: "ArcFace (ResNet34)"
|
|
21673
|
-
},
|
|
21674
|
-
{
|
|
21675
|
-
id: "inception-resnet-v1",
|
|
21676
|
-
label: "Inception ResNet V1"
|
|
21677
|
-
}
|
|
21678
|
-
]
|
|
21502
|
+
options: [{
|
|
21503
|
+
id: "auraface-r100",
|
|
21504
|
+
label: "AuraFace R100"
|
|
21505
|
+
}]
|
|
21679
21506
|
}, {
|
|
21680
21507
|
stepId: "clip-embedding",
|
|
21681
21508
|
label: "Semantic search model",
|
|
21682
|
-
defaultModelId: "
|
|
21509
|
+
defaultModelId: "siglip2-b16-224",
|
|
21683
21510
|
indexName: "the object semantic-search index",
|
|
21684
|
-
options: [
|
|
21685
|
-
|
|
21686
|
-
|
|
21687
|
-
|
|
21688
|
-
},
|
|
21689
|
-
{
|
|
21690
|
-
id: "mobileclip-s2",
|
|
21691
|
-
label: "MobileCLIP S2"
|
|
21692
|
-
},
|
|
21693
|
-
{
|
|
21694
|
-
id: "siglip2-b16-224",
|
|
21695
|
-
label: "SigLIP2 B/16 (Apache-2.0)"
|
|
21696
|
-
}
|
|
21697
|
-
]
|
|
21511
|
+
options: [{
|
|
21512
|
+
id: "siglip2-b16-224",
|
|
21513
|
+
label: "SigLIP2 B/16 (Apache-2.0)"
|
|
21514
|
+
}]
|
|
21698
21515
|
}].map((s) => [s.stepId, s.defaultModelId])));
|
|
21699
21516
|
var CLUSTER_STEP_SETTING_FIELDS = [{
|
|
21700
21517
|
stepId: "face-embedding",
|
|
@@ -24542,6 +24359,22 @@ var MotionSourceEnum = _enum([
|
|
|
24542
24359
|
"device-activity"
|
|
24543
24360
|
]);
|
|
24544
24361
|
/**
|
|
24362
|
+
* What opened an on-motion detection SESSION: the motion source that passed the
|
|
24363
|
+
* camera's `motionSources` gate, and what raised it (the camera itself, or a
|
|
24364
|
+
* sensor the operator attached — D672). `sensorName` is the attached sensor's
|
|
24365
|
+
* device name as the orchestrator's camera-sensors resolution last read it;
|
|
24366
|
+
* absent when unknown or when the camera itself triggered.
|
|
24367
|
+
*
|
|
24368
|
+
* Carried on the session attach ONLY so the runner's `DetectionPhaseTransition`
|
|
24369
|
+
* can say what started the session (D672 rule 11, amended 2026-10-06). The
|
|
24370
|
+
* runner never decides anything on it.
|
|
24371
|
+
*/
|
|
24372
|
+
var SessionTriggerSchema = object({
|
|
24373
|
+
source: MotionSourceEnum,
|
|
24374
|
+
triggeredBy: MotionTriggeredBySchema,
|
|
24375
|
+
sensorName: string().optional()
|
|
24376
|
+
});
|
|
24377
|
+
/**
|
|
24545
24378
|
* List of motion sources active on a camera. Empty array is valid:
|
|
24546
24379
|
* "no source" — happens for battery cams without firmware motion when
|
|
24547
24380
|
* the operator hasn't opted into the SW analyzer (would hold the
|
|
@@ -24793,6 +24626,14 @@ var RunnerCameraConfigSchema = object({
|
|
|
24793
24626
|
*/
|
|
24794
24627
|
motionSources: MotionSourcesSchema.default(["analyzer"]),
|
|
24795
24628
|
/**
|
|
24629
|
+
* What opened this attach, on a SESSION attach only (see
|
|
24630
|
+
* {@link SessionTriggerSchema}). OPTIONAL with no default: absent on a
|
|
24631
|
+
* standing attach, an occupancy burst, a re-dispatch with no motion event
|
|
24632
|
+
* behind it, and every attach from an orchestrator older than the field — the
|
|
24633
|
+
* runner then names no source on the phase event.
|
|
24634
|
+
*/
|
|
24635
|
+
sessionTrigger: SessionTriggerSchema.optional(),
|
|
24636
|
+
/**
|
|
24796
24637
|
* WHICH root detector runs for this camera. The detection counterpart of
|
|
24797
24638
|
* {@link motionSources}, same shape and same discipline: a per-camera list
|
|
24798
24639
|
* of sources the system understands, vendor-agnostic, owned here.
|
|
@@ -31016,7 +30857,7 @@ var LocalInterfaceSchema = object({
|
|
|
31016
30857
|
name: string(),
|
|
31017
30858
|
family: _enum(["IPv4", "IPv6"]),
|
|
31018
30859
|
address: string(),
|
|
31019
|
-
/** CIDR shorthand (e.g. `192.168.
|
|
30860
|
+
/** CIDR shorthand (e.g. `192.168.10.10/24`). Empty when OS doesn't expose. */
|
|
31020
30861
|
cidr: string(),
|
|
31021
30862
|
/** OS netmask. Stable across calls so the SDK can hash on it. */
|
|
31022
30863
|
netmask: string(),
|
|
@@ -31070,7 +30911,8 @@ var ConnectionEndpointSchema = object({
|
|
|
31070
30911
|
"lan-ipv4",
|
|
31071
30912
|
"lan-ipv6",
|
|
31072
30913
|
"public",
|
|
31073
|
-
"loopback"
|
|
30914
|
+
"loopback",
|
|
30915
|
+
"app-link"
|
|
31074
30916
|
]),
|
|
31075
30917
|
/**
|
|
31076
30918
|
* Originating-interface kind — `lan`/`wifi`/`docker`/`vpn`/`other`/
|
|
@@ -31139,15 +30981,27 @@ var GetConnectionEndpointsResultSchema = object({
|
|
|
31139
30981
|
localPort: AdvertisedLocalPortSchema
|
|
31140
30982
|
});
|
|
31141
30983
|
/**
|
|
31142
|
-
* The
|
|
31143
|
-
*
|
|
31144
|
-
*
|
|
30984
|
+
* The address flagged for notification links. `baseUrl: null` = AUTO, which
|
|
30985
|
+
* since D709 is the CamStack app link; `resolved` reports the effective value,
|
|
30986
|
+
* so the UI can show it either way.
|
|
31145
30987
|
*/
|
|
31146
30988
|
var NotificationEndpointSchema = object({
|
|
31147
30989
|
/** The operator's explicit choice, or null for AUTO. */
|
|
31148
30990
|
baseUrl: string().nullable(),
|
|
31149
|
-
/**
|
|
31150
|
-
|
|
30991
|
+
/**
|
|
30992
|
+
* The effective flagged address: the explicit choice, else (AUTO) the
|
|
30993
|
+
* CamStack app link `https://camstack.zentik.app` (D709). A pre-D709 hub
|
|
30994
|
+
* answered the public tunnel / mesh here, or null.
|
|
30995
|
+
*/
|
|
30996
|
+
resolved: string().nullable(),
|
|
30997
|
+
/**
|
|
30998
|
+
* The hub origin an app link carries as `hub=` (D709): the hub's best
|
|
30999
|
+
* public HTTPS address — the tunnel, then the mesh (Tailscale MagicDNS) —
|
|
31000
|
+
* picked from this cap's own palette by `pickAppLinkHub`. `null` when the
|
|
31001
|
+
* hub advertises neither; the app link is then minted without `hub=`.
|
|
31002
|
+
* Absent on a pre-D709 hub.
|
|
31003
|
+
*/
|
|
31004
|
+
appLinkHub: string().nullable()
|
|
31151
31005
|
});
|
|
31152
31006
|
/**
|
|
31153
31007
|
* The URLs the SDK / viewer should race for API access. `baseUrls` empty =
|
|
@@ -31201,7 +31055,11 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
|
|
|
31201
31055
|
* Pass `'https'` when the caller is itself loaded over HTTPS
|
|
31202
31056
|
* to avoid mixed-content blocks in the browser. The public
|
|
31203
31057
|
* tunnel always emits `https://` regardless. */
|
|
31204
|
-
scheme: _enum(["http", "https"]).optional()
|
|
31058
|
+
scheme: _enum(["http", "https"]).optional(),
|
|
31059
|
+
/** Append the CamStack app link entry (`kind: 'app-link'`, D709) —
|
|
31060
|
+
* an address notification links can be flagged with, not a hub
|
|
31061
|
+
* route. Default `false`: racers and rankers never see it. */
|
|
31062
|
+
includeAppLink: boolean().optional()
|
|
31205
31063
|
}), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), ViewerEndpointsSchema), method(object({ baseUrls: array(string()).readonly() }), ViewerEndpointsSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" }), method(_void(), TlsStatusSchema), method(object({ reason: string().optional() }), TlsStatusSchema, {
|
|
31206
31064
|
kind: "mutation",
|
|
31207
31065
|
auth: "admin"
|
|
@@ -31462,7 +31320,7 @@ var MeshPeerSchema = object({
|
|
|
31462
31320
|
tags: array(string()).readonly(),
|
|
31463
31321
|
/** True when the peer advertises itself as an exit-node candidate. */
|
|
31464
31322
|
exitNodeOption: boolean(),
|
|
31465
|
-
/** Subnet routes the peer advertises (e.g. `["192.168.
|
|
31323
|
+
/** Subnet routes the peer advertises (e.g. `["192.168.10.0/24"]`). */
|
|
31466
31324
|
advertisedRoutes: array(string()).readonly(),
|
|
31467
31325
|
/** Owner account login (e.g. `foo@example.com`). Null for tagged peers. */
|
|
31468
31326
|
userLogin: string().nullable(),
|
|
@@ -32406,7 +32264,13 @@ var OsdStatusSchema = object({ overlays: array(object({
|
|
|
32406
32264
|
*/
|
|
32407
32265
|
availablePositions: array(OsdPositionEnum).readonly(),
|
|
32408
32266
|
/** Firmware does not allow editing the `text` field (e.g. timestamp). */
|
|
32409
|
-
readOnly: boolean().optional()
|
|
32267
|
+
readOnly: boolean().optional(),
|
|
32268
|
+
/**
|
|
32269
|
+
* Firmware text ceiling for this overlay. Absent = no known limit.
|
|
32270
|
+
* The OSD manager truncates here on every write — never stores its own
|
|
32271
|
+
* copy on the binding.
|
|
32272
|
+
*/
|
|
32273
|
+
maxCharacters: number().int().positive().optional()
|
|
32410
32274
|
})).readonly() });
|
|
32411
32275
|
var OsdOverlayPatchSchema = object({
|
|
32412
32276
|
enabled: boolean().optional(),
|
|
@@ -32457,7 +32321,9 @@ var OsdTemplateBindingSchema = object({
|
|
|
32457
32321
|
valuePath: string().min(1).max(64),
|
|
32458
32322
|
/** Convert TO this unit at render (`tryConvertUnit`). Absent = source unit. */
|
|
32459
32323
|
displayUnit: string().max(16).optional(),
|
|
32460
|
-
maxDecimals: number().int().min(0).max(4).default(1)
|
|
32324
|
+
maxDecimals: number().int().min(0).max(4).default(1),
|
|
32325
|
+
/** Raw value → display text (same shape as slot binding). Empty string hides. */
|
|
32326
|
+
valueMap: record(string(), string()).optional()
|
|
32461
32327
|
});
|
|
32462
32328
|
/**
|
|
32463
32329
|
* A named template: bindings (alias → leaf field) + a parser that
|
|
@@ -32507,18 +32373,11 @@ var OsdSlotBindingSchema = object({
|
|
|
32507
32373
|
source: OsdSourceSchema,
|
|
32508
32374
|
/** `${value}` and `${unit}` are substituted; every occurrence. */
|
|
32509
32375
|
template: string().max(96).default("${value}"),
|
|
32510
|
-
/** Truncate with an ellipsis past this length. Absent = no limit. */
|
|
32511
|
-
maxCharacters: number().int().min(4).max(64).optional(),
|
|
32512
32376
|
/**
|
|
32513
32377
|
* Decimal places for a numeric value. `0` yields an integer — the
|
|
32514
32378
|
* documented workaround for firmwares that reject `.` in overlay text.
|
|
32515
32379
|
*/
|
|
32516
32380
|
maxDecimals: number().int().min(0).max(4).default(1),
|
|
32517
|
-
/**
|
|
32518
|
-
* Appended via `${unit}` when no unit conversion produces a label.
|
|
32519
|
-
* Prefer `source.displayUnit` for converted device-state values.
|
|
32520
|
-
*/
|
|
32521
|
-
unitLabel: string().max(8).optional(),
|
|
32522
32381
|
/** Raw value → display text, e.g. `{"true":"MOTION","false":""}`. */
|
|
32523
32382
|
valueMap: record(string(), string()).optional(),
|
|
32524
32383
|
/** Time windows in which the slot is shown. Absent = always. */
|
|
@@ -32527,9 +32386,7 @@ var OsdSlotBindingSchema = object({
|
|
|
32527
32386
|
* Display gate, in the notification centre's condition vocabulary.
|
|
32528
32387
|
* Only the keys reported by `getConditionSupport` are accepted.
|
|
32529
32388
|
*/
|
|
32530
|
-
conditions: NcConditionsSchema.optional()
|
|
32531
|
-
/** Rendered when the gate is closed or the value unreadable. Empty = hide. */
|
|
32532
|
-
fallbackText: string().max(64).default("")
|
|
32389
|
+
conditions: NcConditionsSchema.optional()
|
|
32533
32390
|
});
|
|
32534
32391
|
/** One camera slot, as the operator sees it: firmware truth + our binding. */
|
|
32535
32392
|
var OsdSlotViewSchema = object({
|
|
@@ -48206,12 +48063,14 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
|
|
|
48206
48063
|
{
|
|
48207
48064
|
id: "addons",
|
|
48208
48065
|
family: "system",
|
|
48209
|
-
label: "Addons",
|
|
48210
|
-
kinds: [
|
|
48211
|
-
|
|
48212
|
-
|
|
48213
|
-
|
|
48214
|
-
|
|
48066
|
+
label: "Addons stopped",
|
|
48067
|
+
kinds: ["addon-crash-loop"]
|
|
48068
|
+
},
|
|
48069
|
+
{
|
|
48070
|
+
id: "addon-updates",
|
|
48071
|
+
family: "system",
|
|
48072
|
+
label: "Addon updates",
|
|
48073
|
+
kinds: ["addon-update-available", "addon-updated"]
|
|
48215
48074
|
},
|
|
48216
48075
|
{
|
|
48217
48076
|
id: "updates",
|
|
@@ -48371,13 +48230,15 @@ var STATE_SUMMARY_SECTION_VARS = [
|
|
|
48371
48230
|
},
|
|
48372
48231
|
{
|
|
48373
48232
|
name: "summaryAddons",
|
|
48374
|
-
label: "Summary: addons",
|
|
48375
|
-
example: "
|
|
48376
|
-
kinds: [
|
|
48377
|
-
|
|
48378
|
-
|
|
48379
|
-
|
|
48380
|
-
|
|
48233
|
+
label: "Summary: addons stopped",
|
|
48234
|
+
example: "hub — recorder stopped after repeated crashes",
|
|
48235
|
+
kinds: ["addon-crash-loop"]
|
|
48236
|
+
},
|
|
48237
|
+
{
|
|
48238
|
+
name: "summaryAddonUpdates",
|
|
48239
|
+
label: "Summary: addon updates",
|
|
48240
|
+
example: "hub:\n• @camstack/addon-recorder 1.2.3 → 1.2.4",
|
|
48241
|
+
kinds: ["addon-update-available", "addon-updated"]
|
|
48381
48242
|
},
|
|
48382
48243
|
{
|
|
48383
48244
|
name: "summaryUpdates",
|
|
@@ -48461,6 +48322,7 @@ var STATE_SUMMARY_VARS = [
|
|
|
48461
48322
|
[...RULE_BODY_ONLY],
|
|
48462
48323
|
[...RULE_TEXT],
|
|
48463
48324
|
([...RULE_TEXT], [...DEVICE_EVENTS]),
|
|
48325
|
+
([...RULE_TEXT], [...COMBINED_ALARM]),
|
|
48464
48326
|
([...RULE_TEXT], [...DEVICE_EVENTS]),
|
|
48465
48327
|
([...RULE_TEXT], [...BATTERY, ...CONSUMABLE]),
|
|
48466
48328
|
([...RULE_TEXT], [...CONSUMABLE]),
|
|
@@ -48501,6 +48363,8 @@ var STATE_SUMMARY_VARS = [
|
|
|
48501
48363
|
[...RULE_TEXT],
|
|
48502
48364
|
[...RULE_TEXT],
|
|
48503
48365
|
[...RULE_TEXT],
|
|
48366
|
+
[...RULE_TEXT],
|
|
48367
|
+
[...RULE_TEXT],
|
|
48504
48368
|
...STATE_SUMMARY_VARS
|
|
48505
48369
|
];
|
|
48506
48370
|
new Set([
|
|
@@ -49081,7 +48945,9 @@ async function convertModel(input, deps) {
|
|
|
49081
48945
|
var ConvertProgressSchema = object({
|
|
49082
48946
|
phase: string(),
|
|
49083
48947
|
pct: number().optional(),
|
|
49084
|
-
detail: string().optional()
|
|
48948
|
+
detail: string().optional(),
|
|
48949
|
+
/** Raw process line (stderr) — UI log pane, not a phase step. */
|
|
48950
|
+
log: string().optional()
|
|
49085
48951
|
});
|
|
49086
48952
|
var ConvertResultLineSchema = object({ result: object({ artifacts: array(ConvertArtifactSchema) }) });
|
|
49087
48953
|
function buildEntry(metadata, artifacts) {
|
|
@@ -49150,29 +49016,57 @@ function buildModelConvertProvider(deps) {
|
|
|
49150
49016
|
//#endregion
|
|
49151
49017
|
//#region src/model-registry.ts
|
|
49152
49018
|
/**
|
|
49153
|
-
*
|
|
49154
|
-
*
|
|
49019
|
+
* Record that `modelId` now has `format` on `nodeId`. Merges into the node's
|
|
49020
|
+
* formats map — never drops a sibling format on the same node.
|
|
49155
49021
|
*/
|
|
49156
|
-
function
|
|
49022
|
+
function upsertAvailabilityFormat(map, modelId, nodeId, format, presence) {
|
|
49023
|
+
const prevNode = map[modelId]?.[nodeId] ?? { formats: {} };
|
|
49157
49024
|
return {
|
|
49158
49025
|
...map,
|
|
49159
49026
|
[modelId]: {
|
|
49160
49027
|
...map[modelId],
|
|
49161
|
-
[nodeId]:
|
|
49028
|
+
[nodeId]: { formats: {
|
|
49029
|
+
...prevNode.formats,
|
|
49030
|
+
[format]: presence
|
|
49031
|
+
} }
|
|
49162
49032
|
}
|
|
49163
49033
|
};
|
|
49164
49034
|
}
|
|
49165
49035
|
/**
|
|
49036
|
+
* @deprecated Prefer {@link upsertAvailabilityFormat}. Kept for call sites that
|
|
49037
|
+
* still pass a single-format legacy shape — folds into multi-format.
|
|
49038
|
+
*/
|
|
49039
|
+
function upsertAvailability(map, modelId, nodeId, entry) {
|
|
49040
|
+
return upsertAvailabilityFormat(map, modelId, nodeId, entry.format, {
|
|
49041
|
+
sha256: entry.sha256,
|
|
49042
|
+
bytes: entry.bytes,
|
|
49043
|
+
at: entry.at
|
|
49044
|
+
});
|
|
49045
|
+
}
|
|
49046
|
+
/**
|
|
49166
49047
|
* Pure registry operations over the custom-model list. Kept side-effect-free
|
|
49167
49048
|
* so they unit-test without a live `BaseAddon` context — the addon class is a
|
|
49168
49049
|
* thin durable-state wrapper around these.
|
|
49169
49050
|
*/
|
|
49170
49051
|
/**
|
|
49171
|
-
* Insert or replace a descriptor keyed by `entry.id
|
|
49172
|
-
*
|
|
49052
|
+
* Insert or replace a descriptor keyed by `entry.id`. When the id already
|
|
49053
|
+
* exists, **formats are merged** (Scrypted multi-format accumulate) — sibling
|
|
49054
|
+
* formats on the prior entry are never dropped. Returns a new array.
|
|
49173
49055
|
*/
|
|
49174
49056
|
function upsertCustomModel(list, descriptor) {
|
|
49175
|
-
|
|
49057
|
+
const prev = list.find((m) => m.entry.id === descriptor.entry.id);
|
|
49058
|
+
const merged = prev === void 0 ? descriptor : {
|
|
49059
|
+
stepId: descriptor.stepId,
|
|
49060
|
+
entry: {
|
|
49061
|
+
...prev.entry,
|
|
49062
|
+
...descriptor.entry,
|
|
49063
|
+
formats: {
|
|
49064
|
+
...prev.entry.formats,
|
|
49065
|
+
...descriptor.entry.formats
|
|
49066
|
+
}
|
|
49067
|
+
}
|
|
49068
|
+
};
|
|
49069
|
+
return [...list.filter((m) => m.entry.id !== descriptor.entry.id), merged];
|
|
49176
49070
|
}
|
|
49177
49071
|
/** Drop the descriptor with the given `entry.id`. Reports whether anything changed. */
|
|
49178
49072
|
function removeCustomModel(list, modelId) {
|
|
@@ -49182,6 +49076,114 @@ function removeCustomModel(list, modelId) {
|
|
|
49182
49076
|
removed: next.length !== list.length
|
|
49183
49077
|
};
|
|
49184
49078
|
}
|
|
49079
|
+
/** Drop every node entry for `modelId` from the availability map. */
|
|
49080
|
+
function clearAvailability(map, modelId) {
|
|
49081
|
+
if (map[modelId] === void 0) return {
|
|
49082
|
+
next: map,
|
|
49083
|
+
hadEntry: false
|
|
49084
|
+
};
|
|
49085
|
+
const next = { ...map };
|
|
49086
|
+
delete next[modelId];
|
|
49087
|
+
return {
|
|
49088
|
+
next,
|
|
49089
|
+
hadEntry: true
|
|
49090
|
+
};
|
|
49091
|
+
}
|
|
49092
|
+
/**
|
|
49093
|
+
* Drop one `(modelId, nodeId, format)` from availability. Empty nodes and empty
|
|
49094
|
+
* model rows are pruned so Library "On nodes" does not keep a ghost chip.
|
|
49095
|
+
*/
|
|
49096
|
+
function clearAvailabilityFormat(map, modelId, nodeId, format) {
|
|
49097
|
+
const perNode = map[modelId];
|
|
49098
|
+
if (perNode === void 0) return {
|
|
49099
|
+
next: map,
|
|
49100
|
+
cleared: false
|
|
49101
|
+
};
|
|
49102
|
+
const node = perNode[nodeId];
|
|
49103
|
+
if (node === void 0 || node.formats[format] === void 0) return {
|
|
49104
|
+
next: map,
|
|
49105
|
+
cleared: false
|
|
49106
|
+
};
|
|
49107
|
+
const formats = { ...node.formats };
|
|
49108
|
+
delete formats[format];
|
|
49109
|
+
const nextNodes = { ...perNode };
|
|
49110
|
+
if (Object.keys(formats).length === 0) delete nextNodes[nodeId];
|
|
49111
|
+
else nextNodes[nodeId] = { formats };
|
|
49112
|
+
if (Object.keys(nextNodes).length === 0) {
|
|
49113
|
+
const next = { ...map };
|
|
49114
|
+
delete next[modelId];
|
|
49115
|
+
return {
|
|
49116
|
+
next,
|
|
49117
|
+
cleared: true
|
|
49118
|
+
};
|
|
49119
|
+
}
|
|
49120
|
+
return {
|
|
49121
|
+
next: {
|
|
49122
|
+
...map,
|
|
49123
|
+
[modelId]: nextNodes
|
|
49124
|
+
},
|
|
49125
|
+
cleared: true
|
|
49126
|
+
};
|
|
49127
|
+
}
|
|
49128
|
+
/**
|
|
49129
|
+
* Drop availability rows whose modelId is not in the Library registry.
|
|
49130
|
+
* Orphans left Sources showing "imported" after a registry-only wipe.
|
|
49131
|
+
*/
|
|
49132
|
+
function pruneOrphanAvailability(map, registeredIds) {
|
|
49133
|
+
const pruned = [];
|
|
49134
|
+
let next = null;
|
|
49135
|
+
for (const modelId of Object.keys(map)) {
|
|
49136
|
+
if (registeredIds.has(modelId)) continue;
|
|
49137
|
+
if (next === null) next = { ...map };
|
|
49138
|
+
delete next[modelId];
|
|
49139
|
+
pruned.push(modelId);
|
|
49140
|
+
}
|
|
49141
|
+
return {
|
|
49142
|
+
next: next ?? map,
|
|
49143
|
+
pruned
|
|
49144
|
+
};
|
|
49145
|
+
}
|
|
49146
|
+
function stripModelIdFromInferenceDevices(devices, modelId) {
|
|
49147
|
+
let cleared = 0;
|
|
49148
|
+
const next = {};
|
|
49149
|
+
for (const [key, device] of Object.entries(devices)) {
|
|
49150
|
+
const stepsIn = device.steps;
|
|
49151
|
+
if (stepsIn === void 0) {
|
|
49152
|
+
next[key] = {
|
|
49153
|
+
enabled: device.enabled,
|
|
49154
|
+
...device.weight !== void 0 ? { weight: device.weight } : {},
|
|
49155
|
+
...device.maxSessions !== void 0 ? { maxSessions: device.maxSessions } : {}
|
|
49156
|
+
};
|
|
49157
|
+
continue;
|
|
49158
|
+
}
|
|
49159
|
+
const stepsOut = {};
|
|
49160
|
+
for (const [stepId, step] of Object.entries(stepsIn)) {
|
|
49161
|
+
if (step.modelId === modelId) {
|
|
49162
|
+
cleared += 1;
|
|
49163
|
+
const kept = {};
|
|
49164
|
+
if (step.settings !== void 0) kept.settings = { ...step.settings };
|
|
49165
|
+
if (step.jumpDeviceKey !== void 0) kept.jumpDeviceKey = step.jumpDeviceKey;
|
|
49166
|
+
if (Object.keys(kept).length > 0) stepsOut[stepId] = kept;
|
|
49167
|
+
continue;
|
|
49168
|
+
}
|
|
49169
|
+
stepsOut[stepId] = {
|
|
49170
|
+
...step.modelId !== void 0 ? { modelId: step.modelId } : {},
|
|
49171
|
+
...step.settings !== void 0 ? { settings: { ...step.settings } } : {},
|
|
49172
|
+
...step.jumpDeviceKey !== void 0 ? { jumpDeviceKey: step.jumpDeviceKey } : {}
|
|
49173
|
+
};
|
|
49174
|
+
}
|
|
49175
|
+
next[key] = {
|
|
49176
|
+
enabled: device.enabled,
|
|
49177
|
+
...device.weight !== void 0 ? { weight: device.weight } : {},
|
|
49178
|
+
...device.maxSessions !== void 0 ? { maxSessions: device.maxSessions } : {},
|
|
49179
|
+
...Object.keys(stepsOut).length > 0 ? { steps: stepsOut } : {}
|
|
49180
|
+
};
|
|
49181
|
+
}
|
|
49182
|
+
return {
|
|
49183
|
+
next,
|
|
49184
|
+
cleared
|
|
49185
|
+
};
|
|
49186
|
+
}
|
|
49185
49187
|
//#endregion
|
|
49186
49188
|
//#region src/frigate/frigate-import.ts
|
|
49187
49189
|
/**
|
|
@@ -49417,6 +49419,64 @@ async function runPlusConversions(deps, args) {
|
|
|
49417
49419
|
return outcomes;
|
|
49418
49420
|
}
|
|
49419
49421
|
//#endregion
|
|
49422
|
+
//#region src/ensure-formats.ts
|
|
49423
|
+
var ENGINE_FORMATS = ["openvino", "coreml"];
|
|
49424
|
+
function missingEngineFormats(entry) {
|
|
49425
|
+
return ENGINE_FORMATS.filter((f) => entry.formats[f] === void 0);
|
|
49426
|
+
}
|
|
49427
|
+
async function ensureEngineFormats(entry, deps, opts = {}) {
|
|
49428
|
+
if (entry.formats.onnx === void 0) return {
|
|
49429
|
+
entry,
|
|
49430
|
+
conversions: []
|
|
49431
|
+
};
|
|
49432
|
+
const missing = (opts.targetFormats ?? ENGINE_FORMATS).filter((f) => entry.formats[f] === void 0);
|
|
49433
|
+
if (missing.length === 0) return {
|
|
49434
|
+
entry,
|
|
49435
|
+
conversions: []
|
|
49436
|
+
};
|
|
49437
|
+
return {
|
|
49438
|
+
entry,
|
|
49439
|
+
conversions: await runPlusConversions(deps, {
|
|
49440
|
+
entry,
|
|
49441
|
+
targetFormats: missing
|
|
49442
|
+
})
|
|
49443
|
+
};
|
|
49444
|
+
}
|
|
49445
|
+
/**
|
|
49446
|
+
* Ensure OV/CoreML from ONNX when missing, then push every declared format to
|
|
49447
|
+
* every agent (hub is skipped — it already hosts imports).
|
|
49448
|
+
*/
|
|
49449
|
+
async function ensureFormatsThenDistribute(modelId, deps) {
|
|
49450
|
+
const initial = await deps.getEntry();
|
|
49451
|
+
if (initial === void 0) throw new Error(`ensureFormatsThenDistribute: model "${modelId}" is not registered`);
|
|
49452
|
+
let conversions = [];
|
|
49453
|
+
if (initial.formats.onnx !== void 0 && missingEngineFormats(initial).length > 0) conversions = (await ensureEngineFormats(initial, deps)).conversions;
|
|
49454
|
+
const entry = await deps.getEntry() ?? initial;
|
|
49455
|
+
const formats = Object.keys(entry.formats).filter((f) => entry.formats[f] !== void 0);
|
|
49456
|
+
const agents = await deps.listAgentNodeIds();
|
|
49457
|
+
const distributed = [];
|
|
49458
|
+
for (const nodeId of agents) for (const format of formats) try {
|
|
49459
|
+
const res = await deps.distribute(modelId, nodeId, format);
|
|
49460
|
+
distributed.push({
|
|
49461
|
+
nodeId,
|
|
49462
|
+
format,
|
|
49463
|
+
ok: true,
|
|
49464
|
+
bytes: res.bytes
|
|
49465
|
+
});
|
|
49466
|
+
} catch (err) {
|
|
49467
|
+
distributed.push({
|
|
49468
|
+
nodeId,
|
|
49469
|
+
format,
|
|
49470
|
+
ok: false,
|
|
49471
|
+
error: err instanceof Error ? err.message : String(err)
|
|
49472
|
+
});
|
|
49473
|
+
}
|
|
49474
|
+
return {
|
|
49475
|
+
conversions,
|
|
49476
|
+
distributed
|
|
49477
|
+
};
|
|
49478
|
+
}
|
|
49479
|
+
//#endregion
|
|
49420
49480
|
//#region src/frigate/frigate-plus.ts
|
|
49421
49481
|
var PLUS_API_HOST = "https://api.frigate.video";
|
|
49422
49482
|
var PLUS_KEY_RE = /^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}:[a-z0-9]{40}$/;
|
|
@@ -49520,6 +49580,12 @@ var PLUS_TYPE_MAP = {
|
|
|
49520
49580
|
postprocessor: "ssd",
|
|
49521
49581
|
extension: ".tflite"
|
|
49522
49582
|
},
|
|
49583
|
+
/** List API uses `ssd` for MobileDet rows. */
|
|
49584
|
+
ssd: {
|
|
49585
|
+
format: "tflite",
|
|
49586
|
+
postprocessor: "ssd",
|
|
49587
|
+
extension: ".tflite"
|
|
49588
|
+
},
|
|
49523
49589
|
yolonas: {
|
|
49524
49590
|
format: "onnx",
|
|
49525
49591
|
postprocessor: "yolonas",
|
|
@@ -49528,6 +49594,10 @@ var PLUS_TYPE_MAP = {
|
|
|
49528
49594
|
yolov9: {
|
|
49529
49595
|
format: "onnx",
|
|
49530
49596
|
extension: ".onnx"
|
|
49597
|
+
},
|
|
49598
|
+
"yolo-generic": {
|
|
49599
|
+
format: "onnx",
|
|
49600
|
+
extension: ".onnx"
|
|
49531
49601
|
}
|
|
49532
49602
|
};
|
|
49533
49603
|
function plusTypeTraits(type) {
|
|
@@ -49552,7 +49622,16 @@ function orderedLabels(labelMap) {
|
|
|
49552
49622
|
* Distribute.
|
|
49553
49623
|
*/
|
|
49554
49624
|
function plusInfoToDescriptor(info, opts) {
|
|
49555
|
-
const
|
|
49625
|
+
const base = plusTypeTraits(info.type);
|
|
49626
|
+
const traits = opts.format === "tflite" ? {
|
|
49627
|
+
format: "tflite",
|
|
49628
|
+
extension: ".tflite",
|
|
49629
|
+
...info.type === "ssd" || info.type === "mobiledet" ? { postprocessor: "ssd" } : {}
|
|
49630
|
+
} : opts.format === "onnx" ? {
|
|
49631
|
+
format: "onnx",
|
|
49632
|
+
extension: ".onnx",
|
|
49633
|
+
...base.postprocessor !== void 0 ? { postprocessor: base.postprocessor } : {}
|
|
49634
|
+
} : base;
|
|
49556
49635
|
const labels = orderedLabels(info.labelMap);
|
|
49557
49636
|
return {
|
|
49558
49637
|
stepId: "object-detection",
|
|
@@ -49670,15 +49749,33 @@ var ModelUploadStore = class {
|
|
|
49670
49749
|
* only the read-array `listModels` (auto-concatenated across providers); the
|
|
49671
49750
|
* mutations live here so the cap stays a clean collection.
|
|
49672
49751
|
*/
|
|
49673
|
-
/** One
|
|
49674
|
-
var
|
|
49675
|
-
format: _enum(MODEL_FORMATS),
|
|
49752
|
+
/** One format's presence on a node. */
|
|
49753
|
+
var ModelFormatPresenceSchema = object({
|
|
49676
49754
|
sha256: string(),
|
|
49677
49755
|
bytes: number(),
|
|
49678
49756
|
at: number()
|
|
49679
49757
|
});
|
|
49680
|
-
/**
|
|
49758
|
+
/**
|
|
49759
|
+
* Per-node availability — multi-format. Legacy durable rows were
|
|
49760
|
+
* `{ format, sha256, bytes, at }`; preprocess folds them into `formats`.
|
|
49761
|
+
*/
|
|
49762
|
+
var ModelNodeAvailabilitySchema = preprocess((raw) => {
|
|
49763
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return raw;
|
|
49764
|
+
const rec = raw;
|
|
49765
|
+
if (typeof rec.formats === "object" && rec.formats !== null && !Array.isArray(rec.formats)) return { formats: rec.formats };
|
|
49766
|
+
if (typeof rec.format === "string") return { formats: { [rec.format]: {
|
|
49767
|
+
sha256: typeof rec.sha256 === "string" ? rec.sha256 : "",
|
|
49768
|
+
bytes: typeof rec.bytes === "number" ? rec.bytes : 0,
|
|
49769
|
+
at: typeof rec.at === "number" ? rec.at : 0
|
|
49770
|
+
} } };
|
|
49771
|
+
return raw;
|
|
49772
|
+
}, object({ formats: partialRecord(_enum(MODEL_FORMATS), ModelFormatPresenceSchema) }));
|
|
49773
|
+
/** `modelId → nodeId → { formats: { [format]: presence } }`. */
|
|
49681
49774
|
var ModelAvailabilityMapSchema = record(string(), record(string(), ModelNodeAvailabilitySchema));
|
|
49775
|
+
/** Stable Library id for a Frigate+ training (chip id ↔ registry). */
|
|
49776
|
+
function frigatePlusRegistryId(plusModelId) {
|
|
49777
|
+
return `frigate-plus-${plusModelId}`;
|
|
49778
|
+
}
|
|
49682
49779
|
var ScryptedManifestFileSchema = object({
|
|
49683
49780
|
relativePath: string(),
|
|
49684
49781
|
size: number(),
|
|
@@ -49751,8 +49848,75 @@ var FrigatePlusModelSchema = object({
|
|
|
49751
49848
|
height: number().optional()
|
|
49752
49849
|
});
|
|
49753
49850
|
var modelStudioActions = defineCustomActions({
|
|
49754
|
-
registerCustomModel: customAction(CustomModelDescriptorSchema, object({
|
|
49755
|
-
|
|
49851
|
+
registerCustomModel: customAction(CustomModelDescriptorSchema, object({
|
|
49852
|
+
ok: literal(true),
|
|
49853
|
+
conversions: array(object({
|
|
49854
|
+
format: _enum(["openvino", "coreml"]),
|
|
49855
|
+
ok: boolean(),
|
|
49856
|
+
nodeId: string().optional(),
|
|
49857
|
+
error: string().optional()
|
|
49858
|
+
})).readonly()
|
|
49859
|
+
}), { kind: "mutation" }),
|
|
49860
|
+
/**
|
|
49861
|
+
* Ensure OV/CoreML from ONNX when missing, then distribute every declared
|
|
49862
|
+
* format to every online agent (hub skipped).
|
|
49863
|
+
*/
|
|
49864
|
+
ensureFormatsThenDistribute: customAction(object({ modelId: string() }), object({
|
|
49865
|
+
conversions: array(object({
|
|
49866
|
+
format: _enum(["openvino", "coreml"]),
|
|
49867
|
+
ok: boolean(),
|
|
49868
|
+
nodeId: string().optional(),
|
|
49869
|
+
error: string().optional()
|
|
49870
|
+
})).readonly(),
|
|
49871
|
+
distributed: array(object({
|
|
49872
|
+
nodeId: string(),
|
|
49873
|
+
format: _enum(MODEL_FORMATS),
|
|
49874
|
+
ok: boolean(),
|
|
49875
|
+
error: string().optional(),
|
|
49876
|
+
bytes: number().optional()
|
|
49877
|
+
})).readonly()
|
|
49878
|
+
}), { kind: "mutation" }),
|
|
49879
|
+
/**
|
|
49880
|
+
* Convert missing OpenVINO / CoreML from an already-registered ONNX entry
|
|
49881
|
+
* (Frigate+ / Custom per-format Import). Only the requested formats run.
|
|
49882
|
+
*/
|
|
49883
|
+
ensureModelFormats: customAction(object({
|
|
49884
|
+
modelId: string(),
|
|
49885
|
+
formats: array(_enum(["openvino", "coreml"])).min(1).readonly()
|
|
49886
|
+
}), object({ conversions: array(object({
|
|
49887
|
+
format: _enum(["openvino", "coreml"]),
|
|
49888
|
+
ok: boolean(),
|
|
49889
|
+
nodeId: string().optional(),
|
|
49890
|
+
error: string().optional()
|
|
49891
|
+
})).readonly() }), { kind: "mutation" }),
|
|
49892
|
+
removeCustomModel: customAction(object({ modelId: string() }), object({
|
|
49893
|
+
removed: boolean(),
|
|
49894
|
+
deletedNodes: array(string()).readonly(),
|
|
49895
|
+
clearedPins: number().int().nonnegative()
|
|
49896
|
+
}), { kind: "mutation" }),
|
|
49897
|
+
/**
|
|
49898
|
+
* Delete one format of a Library model from one node. Keeps the registry row
|
|
49899
|
+
* and sibling formats/nodes; clears only that availability chip.
|
|
49900
|
+
*/
|
|
49901
|
+
removeModelFromNode: customAction(object({
|
|
49902
|
+
modelId: string(),
|
|
49903
|
+
nodeId: string(),
|
|
49904
|
+
format: _enum(MODEL_FORMATS)
|
|
49905
|
+
}), object({
|
|
49906
|
+
deleted: boolean(),
|
|
49907
|
+
clearedAvailability: boolean()
|
|
49908
|
+
}), { kind: "mutation" }),
|
|
49909
|
+
/**
|
|
49910
|
+
* Wipe one CamStack seed format from a node's modelsDir (Installed Remove).
|
|
49911
|
+
* Routes via `nodePin` — `pipelineExecutor.deleteModel` has no `nodeId` in
|
|
49912
|
+
* its input schema, so a bare tRPC mutate always hit the hub.
|
|
49913
|
+
*/
|
|
49914
|
+
deleteSeedFromNode: customAction(object({
|
|
49915
|
+
addonId: string(),
|
|
49916
|
+
modelId: string(),
|
|
49917
|
+
format: _enum(MODEL_FORMATS),
|
|
49918
|
+
nodeId: string()
|
|
49919
|
+
}), object({ success: literal(true) }), { kind: "mutation" }),
|
|
49756
49920
|
listCustomModels: customAction(_void(), array(CustomModelDescriptorSchema).readonly()),
|
|
49757
49921
|
/**
|
|
49758
49922
|
* Distribute a registered model's FORMAT (resident on the hub) to a target
|
|
@@ -49792,17 +49956,16 @@ var modelStudioActions = defineCustomActions({
|
|
|
49792
49956
|
}), { kind: "mutation" }),
|
|
49793
49957
|
abortModelUpload: customAction(object({ uploadId: string() }), object({ ok: literal(true) }), { kind: "mutation" }),
|
|
49794
49958
|
/**
|
|
49795
|
-
* Curated PUBLIC Frigate model catalog (D214 addendum)
|
|
49796
|
-
*
|
|
49797
|
-
*
|
|
49798
|
-
* the hub's modelsDir and registers the descriptor with the public URL
|
|
49799
|
-
* (other nodes download on demand — no Distribute needed).
|
|
49959
|
+
* Curated PUBLIC Frigate model catalog (D214 addendum). Import registers the
|
|
49960
|
+
* descriptor with its public URL (Library only). Optional `targetNodeId`
|
|
49961
|
+
* downloads on that node — no hub staging disk.
|
|
49800
49962
|
*/
|
|
49801
49963
|
listFrigateCatalog: customAction(_void(), array(FrigateCatalogEntrySchema).readonly()),
|
|
49802
49964
|
importFrigateCatalogModel: customAction(object({
|
|
49803
49965
|
catalogId: string(),
|
|
49804
49966
|
modelId: string().optional(),
|
|
49805
|
-
name: string().optional()
|
|
49967
|
+
name: string().optional(),
|
|
49968
|
+
targetNodeId: string().optional()
|
|
49806
49969
|
}), object({
|
|
49807
49970
|
ok: literal(true),
|
|
49808
49971
|
modelId: string(),
|
|
@@ -49811,13 +49974,17 @@ var modelStudioActions = defineCustomActions({
|
|
|
49811
49974
|
/**
|
|
49812
49975
|
* Local Scrypted YOLO catalog (Task 7B). Listing is a projection of the
|
|
49813
49976
|
* revision-pinned manifesto plus a read-only HF drift check. Import accepts
|
|
49814
|
-
*
|
|
49977
|
+
* `manifestId` (+ optional land-on-node) — host/repo/revision/path/labels/
|
|
49978
|
+
* checksum stay server-owned.
|
|
49815
49979
|
*/
|
|
49816
49980
|
listScryptedCatalog: customAction(_void(), object({
|
|
49817
49981
|
entries: array(ScryptedManifestEntrySchema).readonly(),
|
|
49818
49982
|
drift: array(ScryptedCatalogDriftSchema).readonly()
|
|
49819
49983
|
})),
|
|
49820
|
-
importScryptedCatalogModel: customAction(object({
|
|
49984
|
+
importScryptedCatalogModel: customAction(object({
|
|
49985
|
+
manifestId: string(),
|
|
49986
|
+
targetNodeId: string().optional()
|
|
49987
|
+
}), CustomModelDescriptorSchema, { kind: "mutation" }),
|
|
49821
49988
|
/**
|
|
49822
49989
|
* Frigate+ personal models. The API key lives in the addon's SETTINGS
|
|
49823
49990
|
* (password field) and never leaves the hub: list / test / import all run
|
|
@@ -49827,9 +49994,9 @@ var modelStudioActions = defineCustomActions({
|
|
|
49827
49994
|
/**
|
|
49828
49995
|
* Nodes whose engine can actually LOAD a tflite artifact (the pool's only
|
|
49829
49996
|
* tflite runtime binds the Coral EdgeTPU delegate — no Coral, no tflite).
|
|
49830
|
-
*
|
|
49997
|
+
* Empty object — `addons.custom` cannot carry zod `void`.
|
|
49831
49998
|
*/
|
|
49832
|
-
tfliteSupport: customAction(
|
|
49999
|
+
tfliteSupport: customAction(object({}).optional(), object({ nodes: array(string()).readonly() })),
|
|
49833
50000
|
frigatePlusStatus: customAction(_void(), object({
|
|
49834
50001
|
configured: boolean(),
|
|
49835
50002
|
keyFormatValid: boolean()
|
|
@@ -49841,8 +50008,21 @@ var modelStudioActions = defineCustomActions({
|
|
|
49841
50008
|
frigatePlusListModels: customAction(_void(), array(FrigatePlusModelSchema).readonly()),
|
|
49842
50009
|
importFrigatePlusModel: customAction(object({
|
|
49843
50010
|
plusModelId: string(),
|
|
49844
|
-
|
|
49845
|
-
|
|
50011
|
+
/** Ignored for identity — registry id is always `frigate-plus-<plusModelId>`. */
|
|
50012
|
+
modelId: string().optional(),
|
|
50013
|
+
name: string().optional(),
|
|
50014
|
+
/**
|
|
50015
|
+
* Native artifact class from the list row (`plusDownloadTraits`). The
|
|
50016
|
+
* model-detail API does not always carry `supportedDetectors`, so the
|
|
50017
|
+
* browser passes what the catalog already knew (fixes ssd/yolo-generic
|
|
50018
|
+
* EdgeTPU rows that `plusTypeTraits` alone would call onnx).
|
|
50019
|
+
*/
|
|
50020
|
+
format: _enum(["onnx", "tflite"]).optional(),
|
|
50021
|
+
/**
|
|
50022
|
+
* Engine formats to convert after the native artifact lands. Empty =
|
|
50023
|
+
* download + register only (per-format Import clicks OV/CoreML later).
|
|
50024
|
+
*/
|
|
50025
|
+
ensureFormats: array(_enum(["openvino", "coreml"])).readonly().optional()
|
|
49846
50026
|
}), object({
|
|
49847
50027
|
ok: literal(true),
|
|
49848
50028
|
modelId: string(),
|
|
@@ -49925,6 +50105,15 @@ function spawnRunConvert(jobPath, pythonPath, onProgress, _sessionId) {
|
|
|
49925
50105
|
});
|
|
49926
50106
|
proc.stderr.on("data", (chunk) => {
|
|
49927
50107
|
stderrChunks.push(chunk);
|
|
50108
|
+
for (const line of chunk.toString().split("\n")) {
|
|
50109
|
+
const trimmed = line.trimEnd();
|
|
50110
|
+
if (trimmed.length === 0) continue;
|
|
50111
|
+
onProgress({
|
|
50112
|
+
phase: "log",
|
|
50113
|
+
log: trimmed,
|
|
50114
|
+
detail: trimmed
|
|
50115
|
+
});
|
|
50116
|
+
}
|
|
49928
50117
|
});
|
|
49929
50118
|
proc.on("close", (code) => {
|
|
49930
50119
|
if (code !== 0) {
|
|
@@ -50014,6 +50203,7 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
|
50014
50203
|
phase: p.phase,
|
|
50015
50204
|
pct: p.pct,
|
|
50016
50205
|
detail: p.detail,
|
|
50206
|
+
log: p.log,
|
|
50017
50207
|
sessionId
|
|
50018
50208
|
}
|
|
50019
50209
|
});
|
|
@@ -50057,11 +50247,21 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
|
50057
50247
|
return result;
|
|
50058
50248
|
},
|
|
50059
50249
|
abortModelUpload: async (input) => (await this.getUploadStore()).abort(input),
|
|
50060
|
-
registerCustomModel: async (input) => this.registerCustomModel(input),
|
|
50250
|
+
registerCustomModel: async (input) => this.registerCustomModel(input, { ensureEngines: true }),
|
|
50251
|
+
ensureFormatsThenDistribute: async (input) => this.ensureFormatsThenDistribute_(input.modelId),
|
|
50252
|
+
ensureModelFormats: async (input) => this.ensureModelFormats_(input.modelId, input.formats),
|
|
50061
50253
|
removeCustomModel: async (input) => this.removeCustomModel(input),
|
|
50062
|
-
|
|
50254
|
+
removeModelFromNode: async (input) => this.removeModelFromNode(input),
|
|
50255
|
+
deleteSeedFromNode: async (input) => this.deleteSeedFromNode(input),
|
|
50256
|
+
listCustomModels: async () => {
|
|
50257
|
+
await this.pruneOrphanAvailability_();
|
|
50258
|
+
return this.listCustomModels();
|
|
50259
|
+
},
|
|
50063
50260
|
distributeModel: async (input) => this.distributeModel(input),
|
|
50064
|
-
listAvailability: async () =>
|
|
50261
|
+
listAvailability: async () => {
|
|
50262
|
+
await this.pruneOrphanAvailability_();
|
|
50263
|
+
return this.availability_.get();
|
|
50264
|
+
},
|
|
50065
50265
|
listFrigateCatalog: async () => FRIGATE_PUBLIC_CATALOG.map((c) => ({
|
|
50066
50266
|
id: c.id,
|
|
50067
50267
|
label: c.label,
|
|
@@ -50206,10 +50406,37 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
|
50206
50406
|
async distributeModel(input) {
|
|
50207
50407
|
const descriptor = (await this.state_.get()).find((m) => m.entry.id === input.modelId);
|
|
50208
50408
|
if (!descriptor) throw new Error(`distributeModel: model "${input.modelId}" is not registered`);
|
|
50409
|
+
const formatEntry = descriptor.entry.formats[input.format];
|
|
50410
|
+
if (formatEntry === void 0) throw new Error(`distributeModel: model "${input.modelId}" has no ${input.format} format in the Library`);
|
|
50209
50411
|
const api = this.ctx.api;
|
|
50210
50412
|
if (!api) throw new Error("distributeModel: ctx.api unavailable");
|
|
50413
|
+
if (/^https?:\/\//i.test(formatEntry.url)) {
|
|
50414
|
+
const downloaded = await api.pipelineExecutor.downloadModel.mutate({
|
|
50415
|
+
modelId: input.modelId,
|
|
50416
|
+
format: input.format,
|
|
50417
|
+
addonId: descriptor.stepId
|
|
50418
|
+
}, nodePin(input.nodeId));
|
|
50419
|
+
const bytes = Math.round(downloaded.sizeMB * 1e6);
|
|
50420
|
+
await this.availability_.update((prev) => upsertAvailability(prev, input.modelId, input.nodeId, {
|
|
50421
|
+
format: input.format,
|
|
50422
|
+
sha256: "",
|
|
50423
|
+
bytes,
|
|
50424
|
+
at: Date.now()
|
|
50425
|
+
}));
|
|
50426
|
+
this.ctx.logger.info("Downloaded custom model on node", { meta: {
|
|
50427
|
+
modelId: input.modelId,
|
|
50428
|
+
nodeId: input.nodeId,
|
|
50429
|
+
format: input.format,
|
|
50430
|
+
bytes
|
|
50431
|
+
} });
|
|
50432
|
+
return {
|
|
50433
|
+
ok: true,
|
|
50434
|
+
sha256: "",
|
|
50435
|
+
bytes
|
|
50436
|
+
};
|
|
50437
|
+
}
|
|
50211
50438
|
const res = await api.modelDistributor.distributeModel.mutate({
|
|
50212
|
-
|
|
50439
|
+
targetNodeId: input.nodeId,
|
|
50213
50440
|
modelId: input.modelId,
|
|
50214
50441
|
format: input.format,
|
|
50215
50442
|
entry: descriptor.entry
|
|
@@ -50288,59 +50515,49 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
|
50288
50515
|
}
|
|
50289
50516
|
}
|
|
50290
50517
|
/**
|
|
50291
|
-
*
|
|
50518
|
+
* Register one manifesto entry by id into the Library. Host/repo/revision
|
|
50519
|
+
* stay server-owned. Artifacts are NOT staged on the hub — optional
|
|
50520
|
+
* `targetNodeId` lands the HF URL on that node via download-on-target.
|
|
50292
50521
|
*/
|
|
50293
50522
|
async importScryptedCatalogFromManifest(input) {
|
|
50294
|
-
const
|
|
50295
|
-
|
|
50296
|
-
|
|
50297
|
-
|
|
50298
|
-
|
|
50299
|
-
|
|
50300
|
-
|
|
50301
|
-
|
|
50302
|
-
|
|
50303
|
-
|
|
50304
|
-
|
|
50305
|
-
|
|
50306
|
-
|
|
50307
|
-
|
|
50308
|
-
|
|
50309
|
-
|
|
50310
|
-
|
|
50311
|
-
|
|
50312
|
-
|
|
50313
|
-
|
|
50314
|
-
manifestId,
|
|
50315
|
-
status: "unavailable",
|
|
50316
|
-
details: err instanceof Error ? err.message : "Hugging Face tree unavailable"
|
|
50317
|
-
};
|
|
50318
|
-
}
|
|
50319
|
-
}
|
|
50320
|
-
});
|
|
50523
|
+
const entry = getScryptedManifestEntry(input.manifestId);
|
|
50524
|
+
if (!entry) throw new Error(`importScryptedCatalogModel: unknown manifest id "${input.manifestId}"`);
|
|
50525
|
+
let drift;
|
|
50526
|
+
try {
|
|
50527
|
+
const tree = await fetchScryptedRemoteTree({ fetchImpl: fetch });
|
|
50528
|
+
const row = compareScryptedManifestDrift(SCRYPTED_YOLO_MANIFEST.filter((e) => e.id === input.manifestId), tree)[0];
|
|
50529
|
+
drift = normalizeScryptedDrift(row ?? {
|
|
50530
|
+
manifestId: input.manifestId,
|
|
50531
|
+
status: "missing",
|
|
50532
|
+
details: "entry missing from manifesto"
|
|
50533
|
+
});
|
|
50534
|
+
} catch (err) {
|
|
50535
|
+
drift = {
|
|
50536
|
+
manifestId: input.manifestId,
|
|
50537
|
+
status: "unavailable",
|
|
50538
|
+
details: err instanceof Error ? err.message : "Hugging Face tree unavailable"
|
|
50539
|
+
};
|
|
50540
|
+
}
|
|
50541
|
+
if (drift.status !== "ok") throw new Error(`importScryptedCatalogModel: entry "${entry.id}" is blocked by drift (${drift.status}${drift.details ? `: ${drift.details}` : ""})`);
|
|
50542
|
+
const descriptor = buildScryptedRegistryDescriptor(entry);
|
|
50321
50543
|
await this.registerCustomModel(descriptor);
|
|
50322
|
-
|
|
50323
|
-
|
|
50324
|
-
|
|
50325
|
-
|
|
50326
|
-
|
|
50327
|
-
|
|
50328
|
-
sha256: scryptedBundleFingerprint(manifestEntry),
|
|
50329
|
-
bytes,
|
|
50330
|
-
at: Date.now()
|
|
50331
|
-
}));
|
|
50332
|
-
this.ctx.logger.info("Imported Scrypted catalog model", { meta: {
|
|
50544
|
+
if (input.targetNodeId !== void 0 && input.targetNodeId.length > 0) await this.distributeModel({
|
|
50545
|
+
modelId: descriptor.entry.id,
|
|
50546
|
+
nodeId: input.targetNodeId,
|
|
50547
|
+
format: entry.format
|
|
50548
|
+
});
|
|
50549
|
+
this.ctx.logger.info("Registered Scrypted catalog model", { meta: {
|
|
50333
50550
|
manifestId: input.manifestId,
|
|
50334
50551
|
modelId: descriptor.entry.id,
|
|
50335
|
-
|
|
50552
|
+
format: entry.format,
|
|
50553
|
+
...input.targetNodeId !== void 0 ? { targetNodeId: input.targetNodeId } : {}
|
|
50336
50554
|
} });
|
|
50337
50555
|
return descriptor;
|
|
50338
50556
|
}
|
|
50339
50557
|
/**
|
|
50340
|
-
*
|
|
50341
|
-
*
|
|
50342
|
-
*
|
|
50343
|
-
* and record hub availability.
|
|
50558
|
+
* Register a curated PUBLIC Frigate catalog model into the Library. The
|
|
50559
|
+
* entry keeps its public URL — no hub staging. Optional `targetNodeId`
|
|
50560
|
+
* downloads on that node (same path as Library → Distribute).
|
|
50344
50561
|
*/
|
|
50345
50562
|
async importFrigateCatalogModel(input) {
|
|
50346
50563
|
const cat = FRIGATE_PUBLIC_CATALOG.find((c) => c.id === input.catalogId);
|
|
@@ -50349,29 +50566,21 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
|
50349
50566
|
...input.modelId !== void 0 ? { modelId: input.modelId } : {},
|
|
50350
50567
|
...input.name !== void 0 ? { name: input.name } : {}
|
|
50351
50568
|
});
|
|
50352
|
-
const modelsDir = await this.resolveModelsDir();
|
|
50353
|
-
const filename = cat.url.split("/").at(-1) ?? `${descriptor.entry.id}.${cat.format}`;
|
|
50354
|
-
const destPath = path.join(modelsDir, filename);
|
|
50355
|
-
await downloadFile(cat.url, destPath);
|
|
50356
|
-
const bytes = (await fs.promises.stat(destPath)).size;
|
|
50357
|
-
const declaredBytes = cat.sizeMB * 1e6;
|
|
50358
|
-
if (bytes < declaredBytes * .5 || bytes > declaredBytes * 2) {
|
|
50359
|
-
await fs.promises.rm(destPath, { force: true });
|
|
50360
|
-
throw new Error(`importFrigateCatalogModel: downloaded ${bytes} bytes but the catalog declares ~${Math.round(declaredBytes)} — refusing to register a suspicious artifact`);
|
|
50361
|
-
}
|
|
50362
|
-
const sha256 = await ModelStudioAddon.fileSha256(destPath);
|
|
50363
50569
|
await this.registerCustomModel(descriptor);
|
|
50364
|
-
|
|
50365
|
-
|
|
50366
|
-
|
|
50367
|
-
|
|
50368
|
-
|
|
50369
|
-
|
|
50370
|
-
|
|
50570
|
+
let bytes = Math.round(cat.sizeMB * 1e6);
|
|
50571
|
+
if (input.targetNodeId !== void 0 && input.targetNodeId.length > 0) {
|
|
50572
|
+
const landed = await this.distributeModel({
|
|
50573
|
+
modelId: descriptor.entry.id,
|
|
50574
|
+
nodeId: input.targetNodeId,
|
|
50575
|
+
format: cat.format
|
|
50576
|
+
});
|
|
50577
|
+
bytes = landed.bytes > 0 ? landed.bytes : bytes;
|
|
50578
|
+
}
|
|
50579
|
+
this.ctx.logger.info("Registered Frigate catalog model", { meta: {
|
|
50371
50580
|
catalogId: input.catalogId,
|
|
50372
50581
|
modelId: descriptor.entry.id,
|
|
50373
|
-
|
|
50374
|
-
|
|
50582
|
+
...input.targetNodeId !== void 0 ? { targetNodeId: input.targetNodeId } : {},
|
|
50583
|
+
bytes
|
|
50375
50584
|
} });
|
|
50376
50585
|
return {
|
|
50377
50586
|
ok: true,
|
|
@@ -50387,11 +50596,20 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
|
50387
50596
|
* is hub-local: other nodes get it via Models → Distribute.
|
|
50388
50597
|
*/
|
|
50389
50598
|
async importFrigatePlusModel(input) {
|
|
50390
|
-
|
|
50599
|
+
const modelId = frigatePlusRegistryId(input.plusModelId);
|
|
50391
50600
|
const key = this.config.frigatePlusApiKey ?? "";
|
|
50392
50601
|
const { info, downloadUrl } = await requestPlusModelDownload(plusFetch, key, input.plusModelId);
|
|
50393
|
-
const
|
|
50394
|
-
const
|
|
50602
|
+
const typeTraits = plusTypeTraits(info.type);
|
|
50603
|
+
const traits = input.format === "tflite" ? {
|
|
50604
|
+
format: "tflite",
|
|
50605
|
+
extension: ".tflite",
|
|
50606
|
+
...info.type === "ssd" || info.type === "mobiledet" ? { postprocessor: "ssd" } : {}
|
|
50607
|
+
} : input.format === "onnx" ? {
|
|
50608
|
+
format: "onnx",
|
|
50609
|
+
extension: ".onnx",
|
|
50610
|
+
...typeTraits.postprocessor !== void 0 ? { postprocessor: typeTraits.postprocessor } : {}
|
|
50611
|
+
} : typeTraits;
|
|
50612
|
+
const filename = `${modelId}${traits.extension}`;
|
|
50395
50613
|
const modelsDir = await this.resolveModelsDir();
|
|
50396
50614
|
const destPath = path.join(modelsDir, filename);
|
|
50397
50615
|
await downloadFile(downloadUrl, destPath);
|
|
@@ -50402,13 +50620,14 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
|
50402
50620
|
}
|
|
50403
50621
|
const sha256 = await ModelStudioAddon.fileSha256(destPath);
|
|
50404
50622
|
const descriptor = plusInfoToDescriptor(info, {
|
|
50405
|
-
modelId
|
|
50623
|
+
modelId,
|
|
50406
50624
|
name: input.name?.trim() || `Frigate+ ${info.type} (${input.plusModelId.slice(0, 8)})`,
|
|
50407
50625
|
filename,
|
|
50408
|
-
sizeMB: Number((bytes / 1e6).toFixed(1))
|
|
50626
|
+
sizeMB: Number((bytes / 1e6).toFixed(1)),
|
|
50627
|
+
...input.format !== void 0 ? { format: input.format } : {}
|
|
50409
50628
|
});
|
|
50410
|
-
await this.registerCustomModel(descriptor);
|
|
50411
|
-
await this.availability_.update((prev) => upsertAvailability(prev,
|
|
50629
|
+
await this.registerCustomModel(descriptor, { ensureEngines: false });
|
|
50630
|
+
await this.availability_.update((prev) => upsertAvailability(prev, modelId, "hub", {
|
|
50412
50631
|
format: traits.format,
|
|
50413
50632
|
sha256,
|
|
50414
50633
|
bytes,
|
|
@@ -50416,59 +50635,28 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
|
50416
50635
|
}));
|
|
50417
50636
|
this.ctx.logger.info("Imported Frigate+ model", { meta: {
|
|
50418
50637
|
plusModelId: input.plusModelId,
|
|
50419
|
-
modelId
|
|
50638
|
+
modelId,
|
|
50420
50639
|
type: info.type,
|
|
50421
|
-
bytes
|
|
50640
|
+
bytes,
|
|
50641
|
+
format: traits.format
|
|
50422
50642
|
} });
|
|
50643
|
+
const wanted = input.ensureFormats ?? [];
|
|
50423
50644
|
let conversions = [];
|
|
50424
|
-
if (traits.format === "onnx") {
|
|
50425
|
-
|
|
50426
|
-
|
|
50427
|
-
|
|
50428
|
-
|
|
50429
|
-
|
|
50430
|
-
freshSourceUrl: async () => (await requestPlusModelDownload(plusFetch, key, input.plusModelId)).downloadUrl,
|
|
50431
|
-
upsertEntry: async (entry) => {
|
|
50432
|
-
await this.state_.update((prev) => upsertCustomModel(prev, {
|
|
50433
|
-
stepId: descriptor.stepId,
|
|
50434
|
-
entry
|
|
50435
|
-
}));
|
|
50436
|
-
},
|
|
50437
|
-
markAvailable: async (nId, mId, artifacts) => {
|
|
50438
|
-
await this.availability_.update((prev) => {
|
|
50439
|
-
const seen = /* @__PURE__ */ new Set();
|
|
50440
|
-
let next = prev;
|
|
50441
|
-
for (const artifact of artifacts) {
|
|
50442
|
-
if (seen.has(artifact.format)) continue;
|
|
50443
|
-
seen.add(artifact.format);
|
|
50444
|
-
next = upsertAvailability(next, mId, nId, {
|
|
50445
|
-
format: artifact.format,
|
|
50446
|
-
sha256: "",
|
|
50447
|
-
bytes: Math.round(artifact.sizeMB * 1e6),
|
|
50448
|
-
at: Date.now()
|
|
50449
|
-
});
|
|
50450
|
-
}
|
|
50451
|
-
return next;
|
|
50452
|
-
});
|
|
50453
|
-
}
|
|
50454
|
-
}, {
|
|
50455
|
-
entry: descriptor.entry,
|
|
50456
|
-
targetFormats: ["openvino", "coreml"]
|
|
50457
|
-
});
|
|
50458
|
-
this.ctx.logger.info("Frigate+ import — engine conversions", { meta: {
|
|
50459
|
-
modelId: input.modelId,
|
|
50460
|
-
outcomes: conversions.map((o) => ({ ...o }))
|
|
50461
|
-
} });
|
|
50462
|
-
}
|
|
50645
|
+
if (traits.format === "onnx" && wanted.length > 0) {
|
|
50646
|
+
conversions = (await ensureEngineFormats(descriptor.entry, this.plusConversionDeps(async () => (await requestPlusModelDownload(plusFetch, key, input.plusModelId)).downloadUrl, descriptor.stepId), { targetFormats: wanted })).conversions;
|
|
50647
|
+
this.ctx.logger.info("Frigate+ import — engine conversions", { meta: {
|
|
50648
|
+
modelId,
|
|
50649
|
+
outcomes: conversions.map((o) => ({ ...o }))
|
|
50650
|
+
} });
|
|
50463
50651
|
}
|
|
50464
50652
|
return {
|
|
50465
50653
|
ok: true,
|
|
50466
|
-
modelId
|
|
50654
|
+
modelId,
|
|
50467
50655
|
bytes,
|
|
50468
50656
|
conversions
|
|
50469
50657
|
};
|
|
50470
50658
|
}
|
|
50471
|
-
async registerCustomModel(input) {
|
|
50659
|
+
async registerCustomModel(input, opts = {}) {
|
|
50472
50660
|
if (!input.stepId.trim()) throw new Error("registerCustomModel: stepId is required");
|
|
50473
50661
|
if (!input.entry.id.trim()) throw new Error("registerCustomModel: entry.id is required");
|
|
50474
50662
|
await this.state_.update((prev) => upsertCustomModel(prev, input));
|
|
@@ -50476,9 +50664,252 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
|
50476
50664
|
modelId: input.entry.id,
|
|
50477
50665
|
stepId: input.stepId
|
|
50478
50666
|
} });
|
|
50479
|
-
|
|
50667
|
+
let conversions = [];
|
|
50668
|
+
if (opts.ensureEngines === true && input.entry.formats.onnx !== void 0) {
|
|
50669
|
+
const onnxUrl = input.entry.formats.onnx.url;
|
|
50670
|
+
if (typeof onnxUrl === "string" && onnxUrl.length > 0) {
|
|
50671
|
+
const deps = this.plusConversionDeps(() => Promise.resolve(onnxUrl), input.stepId);
|
|
50672
|
+
conversions = (await ensureEngineFormats(input.entry, deps)).conversions;
|
|
50673
|
+
if (conversions.length > 0) this.ctx.logger.info("Custom model — engine conversions", { meta: {
|
|
50674
|
+
modelId: input.entry.id,
|
|
50675
|
+
outcomes: conversions.map((o) => ({ ...o }))
|
|
50676
|
+
} });
|
|
50677
|
+
}
|
|
50678
|
+
}
|
|
50679
|
+
return {
|
|
50680
|
+
ok: true,
|
|
50681
|
+
conversions
|
|
50682
|
+
};
|
|
50683
|
+
}
|
|
50684
|
+
plusConversionDeps(freshSourceUrl, stepId) {
|
|
50685
|
+
const api = this.ctx.api;
|
|
50686
|
+
return {
|
|
50687
|
+
recommend: (i) => recommendConvertNode(i, this.recommendDeps()),
|
|
50688
|
+
convert: async (convertInput, nId) => {
|
|
50689
|
+
if (!api) throw new Error("modelConvert unavailable");
|
|
50690
|
+
return api.modelConvert.convert.mutate(convertInput, nodePin(nId));
|
|
50691
|
+
},
|
|
50692
|
+
freshSourceUrl,
|
|
50693
|
+
upsertEntry: async (entry) => {
|
|
50694
|
+
await this.state_.update((prev) => upsertCustomModel(prev, {
|
|
50695
|
+
stepId,
|
|
50696
|
+
entry
|
|
50697
|
+
}));
|
|
50698
|
+
},
|
|
50699
|
+
markAvailable: async (nId, mId, artifacts) => {
|
|
50700
|
+
await this.availability_.update((prev) => {
|
|
50701
|
+
const seen = /* @__PURE__ */ new Set();
|
|
50702
|
+
let next = prev;
|
|
50703
|
+
for (const artifact of artifacts) {
|
|
50704
|
+
if (seen.has(artifact.format)) continue;
|
|
50705
|
+
seen.add(artifact.format);
|
|
50706
|
+
next = upsertAvailability(next, mId, nId, {
|
|
50707
|
+
format: artifact.format,
|
|
50708
|
+
sha256: "",
|
|
50709
|
+
bytes: Math.round(artifact.sizeMB * 1e6),
|
|
50710
|
+
at: Date.now()
|
|
50711
|
+
});
|
|
50712
|
+
}
|
|
50713
|
+
return next;
|
|
50714
|
+
});
|
|
50715
|
+
}
|
|
50716
|
+
};
|
|
50717
|
+
}
|
|
50718
|
+
async ensureModelFormats_(modelId, formats) {
|
|
50719
|
+
const row = (await this.state_.get()).find((m) => m.entry.id === modelId);
|
|
50720
|
+
if (row === void 0) throw new Error(`ensureModelFormats: model "${modelId}" is not registered`);
|
|
50721
|
+
const onnxUrl = row.entry.formats.onnx?.url;
|
|
50722
|
+
if (typeof onnxUrl !== "string" || onnxUrl.length === 0) throw new Error(`ensureModelFormats: model "${modelId}" has no ONNX to convert from`);
|
|
50723
|
+
const ensured = await ensureEngineFormats(row.entry, this.plusConversionDeps(async () => {
|
|
50724
|
+
const url = ((await this.state_.get()).find((m) => m.entry.id === modelId)?.entry)?.formats.onnx?.url ?? onnxUrl;
|
|
50725
|
+
if (typeof url !== "string" || url.length === 0) throw new Error(`ensureModelFormats: no ONNX url for "${modelId}"`);
|
|
50726
|
+
return url;
|
|
50727
|
+
}, row.stepId), { targetFormats: formats });
|
|
50728
|
+
this.ctx.logger.info("ensureModelFormats", { meta: {
|
|
50729
|
+
modelId,
|
|
50730
|
+
outcomes: ensured.conversions.map((o) => ({ ...o }))
|
|
50731
|
+
} });
|
|
50732
|
+
return { conversions: ensured.conversions };
|
|
50733
|
+
}
|
|
50734
|
+
async ensureFormatsThenDistribute_(modelId) {
|
|
50735
|
+
const api = this.ctx.api;
|
|
50736
|
+
if (!api) throw new Error("ensureFormatsThenDistribute: ctx.api unavailable");
|
|
50737
|
+
const getEntry = async () => {
|
|
50738
|
+
return (await this.state_.get()).find((m) => m.entry.id === modelId)?.entry;
|
|
50739
|
+
};
|
|
50740
|
+
const entry = await getEntry();
|
|
50741
|
+
if (entry === void 0) throw new Error(`ensureFormatsThenDistribute: model "${modelId}" is not registered`);
|
|
50742
|
+
const onnxUrl = entry.formats.onnx?.url;
|
|
50743
|
+
const stepId = (await this.state_.get()).find((m) => m.entry.id === modelId)?.stepId ?? "object-detection";
|
|
50744
|
+
return ensureFormatsThenDistribute(modelId, {
|
|
50745
|
+
...this.plusConversionDeps(async () => {
|
|
50746
|
+
const url = (await getEntry())?.formats.onnx?.url ?? onnxUrl;
|
|
50747
|
+
if (typeof url !== "string" || url.length === 0) throw new Error(`ensureFormatsThenDistribute: no ONNX url for "${modelId}"`);
|
|
50748
|
+
return url;
|
|
50749
|
+
}, stepId),
|
|
50750
|
+
getEntry,
|
|
50751
|
+
listAgentNodeIds: async () => {
|
|
50752
|
+
return (await api.nodes.directory.query()).nodes.filter((n) => n.isOnline && !n.isHub).map((n) => n.id);
|
|
50753
|
+
},
|
|
50754
|
+
distribute: async (mId, nodeId, format) => {
|
|
50755
|
+
const res = await this.distributeModel({
|
|
50756
|
+
modelId: mId,
|
|
50757
|
+
nodeId,
|
|
50758
|
+
format
|
|
50759
|
+
});
|
|
50760
|
+
return {
|
|
50761
|
+
sha256: res.sha256,
|
|
50762
|
+
bytes: res.bytes
|
|
50763
|
+
};
|
|
50764
|
+
}
|
|
50765
|
+
});
|
|
50480
50766
|
}
|
|
50767
|
+
/**
|
|
50768
|
+
* Delete one CamStack seed format on one node. Physically removes files from
|
|
50769
|
+
* that node's modelsDir via `pipelineExecutor.deleteModel` + `nodePin`.
|
|
50770
|
+
*/
|
|
50771
|
+
async deleteSeedFromNode(input) {
|
|
50772
|
+
const api = this.ctx.api;
|
|
50773
|
+
if (!api) throw new Error("deleteSeedFromNode: API not available");
|
|
50774
|
+
await api.pipelineExecutor.deleteModel.mutate({
|
|
50775
|
+
addonId: input.addonId,
|
|
50776
|
+
modelId: input.modelId,
|
|
50777
|
+
format: input.format
|
|
50778
|
+
}, nodePin(input.nodeId));
|
|
50779
|
+
this.ctx.logger.info("CamStack seed deleted from node disk", { meta: {
|
|
50780
|
+
modelId: input.modelId,
|
|
50781
|
+
format: input.format,
|
|
50782
|
+
nodeId: input.nodeId,
|
|
50783
|
+
addonId: input.addonId
|
|
50784
|
+
} });
|
|
50785
|
+
return { success: true };
|
|
50786
|
+
}
|
|
50787
|
+
/**
|
|
50788
|
+
* Delete one format on one node. Registry row stays; only that availability
|
|
50789
|
+
* chip is cleared. Pins are left alone — re-Send to node restores files.
|
|
50790
|
+
*/
|
|
50791
|
+
async removeModelFromNode(input) {
|
|
50792
|
+
const descriptor = (await this.state_.get()).find((m) => m.entry.id === input.modelId);
|
|
50793
|
+
if (!descriptor) throw new Error(`removeModelFromNode: model "${input.modelId}" is not registered`);
|
|
50794
|
+
if (descriptor.entry.formats[input.format] === void 0) throw new Error(`removeModelFromNode: model "${input.modelId}" has no ${input.format} format in the Library`);
|
|
50795
|
+
let deleted = false;
|
|
50796
|
+
const api = this.ctx.api;
|
|
50797
|
+
try {
|
|
50798
|
+
if (api) {
|
|
50799
|
+
await api.pipelineExecutor.deleteModel.mutate({
|
|
50800
|
+
addonId: descriptor.stepId,
|
|
50801
|
+
modelId: input.modelId,
|
|
50802
|
+
format: input.format
|
|
50803
|
+
}, nodePin(input.nodeId));
|
|
50804
|
+
deleted = true;
|
|
50805
|
+
}
|
|
50806
|
+
} catch (err) {
|
|
50807
|
+
this.ctx.logger.warn("removeModelFromNode: remote delete failed (continuing)", { meta: {
|
|
50808
|
+
modelId: input.modelId,
|
|
50809
|
+
nodeId: input.nodeId,
|
|
50810
|
+
format: input.format,
|
|
50811
|
+
error: err instanceof Error ? err.message : String(err)
|
|
50812
|
+
} });
|
|
50813
|
+
}
|
|
50814
|
+
if (input.nodeId === "hub") try {
|
|
50815
|
+
deleteModelFromDisk(await this.resolveModelsDir(), descriptor.entry, input.format);
|
|
50816
|
+
deleted = true;
|
|
50817
|
+
} catch (err) {
|
|
50818
|
+
this.ctx.logger.warn("removeModelFromNode: hub disk wipe failed", { meta: {
|
|
50819
|
+
modelId: input.modelId,
|
|
50820
|
+
format: input.format,
|
|
50821
|
+
error: err instanceof Error ? err.message : String(err)
|
|
50822
|
+
} });
|
|
50823
|
+
}
|
|
50824
|
+
let clearedAvailability = false;
|
|
50825
|
+
await this.availability_.update((prev) => {
|
|
50826
|
+
const result = clearAvailabilityFormat(prev, input.modelId, input.nodeId, input.format);
|
|
50827
|
+
clearedAvailability = result.cleared;
|
|
50828
|
+
return result.next;
|
|
50829
|
+
});
|
|
50830
|
+
this.ctx.logger.info("Removed custom model format from node", { meta: {
|
|
50831
|
+
modelId: input.modelId,
|
|
50832
|
+
nodeId: input.nodeId,
|
|
50833
|
+
format: input.format,
|
|
50834
|
+
deleted,
|
|
50835
|
+
clearedAvailability
|
|
50836
|
+
} });
|
|
50837
|
+
return {
|
|
50838
|
+
deleted,
|
|
50839
|
+
clearedAvailability
|
|
50840
|
+
};
|
|
50841
|
+
}
|
|
50842
|
+
/**
|
|
50843
|
+
* Full purge of a Library model: delete artifacts on every node that had
|
|
50844
|
+
* them, clear availability (so Sources no longer shows "imported"), strip
|
|
50845
|
+
* pipeline pins that named this modelId (agents revert to hardware defaults
|
|
50846
|
+
* for that step), then drop the registry row.
|
|
50847
|
+
*/
|
|
50481
50848
|
async removeCustomModel(input) {
|
|
50849
|
+
const descriptor = (await this.state_.get()).find((m) => m.entry.id === input.modelId);
|
|
50850
|
+
if (!descriptor) {
|
|
50851
|
+
await this.availability_.update((prev) => clearAvailability(prev, input.modelId).next);
|
|
50852
|
+
return {
|
|
50853
|
+
removed: false,
|
|
50854
|
+
deletedNodes: [],
|
|
50855
|
+
clearedPins: 0
|
|
50856
|
+
};
|
|
50857
|
+
}
|
|
50858
|
+
const formats = Object.keys(descriptor.entry.formats).filter((f) => descriptor.entry.formats[f] !== void 0);
|
|
50859
|
+
const avail = await this.availability_.get();
|
|
50860
|
+
const nodeIds = new Set(["hub", ...Object.keys(avail[input.modelId] ?? {})]);
|
|
50861
|
+
const deletedNodes = [];
|
|
50862
|
+
const api = this.ctx.api;
|
|
50863
|
+
for (const nodeId of nodeIds) {
|
|
50864
|
+
let anyDeleted = false;
|
|
50865
|
+
for (const format of formats) try {
|
|
50866
|
+
if (api) {
|
|
50867
|
+
await api.pipelineExecutor.deleteModel.mutate({
|
|
50868
|
+
addonId: descriptor.stepId,
|
|
50869
|
+
modelId: input.modelId,
|
|
50870
|
+
format
|
|
50871
|
+
}, nodePin(nodeId));
|
|
50872
|
+
anyDeleted = true;
|
|
50873
|
+
}
|
|
50874
|
+
} catch (err) {
|
|
50875
|
+
this.ctx.logger.warn("removeCustomModel: remote delete failed (continuing)", { meta: {
|
|
50876
|
+
modelId: input.modelId,
|
|
50877
|
+
nodeId,
|
|
50878
|
+
format,
|
|
50879
|
+
error: err instanceof Error ? err.message : String(err)
|
|
50880
|
+
} });
|
|
50881
|
+
}
|
|
50882
|
+
if (anyDeleted) deletedNodes.push(nodeId);
|
|
50883
|
+
}
|
|
50884
|
+
try {
|
|
50885
|
+
const modelsDir = await this.resolveModelsDir();
|
|
50886
|
+
for (const format of formats) deleteModelFromDisk(modelsDir, descriptor.entry, format);
|
|
50887
|
+
} catch (err) {
|
|
50888
|
+
this.ctx.logger.warn("removeCustomModel: hub disk wipe failed", { meta: {
|
|
50889
|
+
modelId: input.modelId,
|
|
50890
|
+
error: err instanceof Error ? err.message : String(err)
|
|
50891
|
+
} });
|
|
50892
|
+
}
|
|
50893
|
+
let clearedPins = 0;
|
|
50894
|
+
if (api) {
|
|
50895
|
+
try {
|
|
50896
|
+
clearedPins += await this.clearAgentModelPins(input.modelId);
|
|
50897
|
+
} catch (err) {
|
|
50898
|
+
this.ctx.logger.warn("removeCustomModel: agent pin clear failed", { meta: {
|
|
50899
|
+
modelId: input.modelId,
|
|
50900
|
+
error: err instanceof Error ? err.message : String(err)
|
|
50901
|
+
} });
|
|
50902
|
+
}
|
|
50903
|
+
try {
|
|
50904
|
+
clearedPins += await this.clearCameraModelPins(input.modelId, descriptor.stepId);
|
|
50905
|
+
} catch (err) {
|
|
50906
|
+
this.ctx.logger.warn("removeCustomModel: camera pin clear failed", { meta: {
|
|
50907
|
+
modelId: input.modelId,
|
|
50908
|
+
error: err instanceof Error ? err.message : String(err)
|
|
50909
|
+
} });
|
|
50910
|
+
}
|
|
50911
|
+
}
|
|
50912
|
+
await this.availability_.update((prev) => clearAvailability(prev, input.modelId).next);
|
|
50482
50913
|
let removed = false;
|
|
50483
50914
|
await this.state_.update((prev) => {
|
|
50484
50915
|
const result = removeCustomModel(prev, input.modelId);
|
|
@@ -50487,13 +50918,80 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
|
50487
50918
|
});
|
|
50488
50919
|
this.ctx.logger.info("Removed custom model", { meta: {
|
|
50489
50920
|
modelId: input.modelId,
|
|
50490
|
-
removed
|
|
50921
|
+
removed,
|
|
50922
|
+
deletedNodes,
|
|
50923
|
+
clearedPins
|
|
50491
50924
|
} });
|
|
50492
|
-
return {
|
|
50925
|
+
return {
|
|
50926
|
+
removed,
|
|
50927
|
+
deletedNodes,
|
|
50928
|
+
clearedPins
|
|
50929
|
+
};
|
|
50930
|
+
}
|
|
50931
|
+
/** Strip `modelId` from every agent's inferenceDevices step pins. */
|
|
50932
|
+
async clearAgentModelPins(modelId) {
|
|
50933
|
+
const api = this.ctx.api;
|
|
50934
|
+
if (!api) return 0;
|
|
50935
|
+
const agents = await api.pipelineOrchestrator.listAgentSettings.query();
|
|
50936
|
+
let cleared = 0;
|
|
50937
|
+
for (const agent of agents) {
|
|
50938
|
+
const devices = agent.settings.inferenceDevices;
|
|
50939
|
+
if (devices === void 0) continue;
|
|
50940
|
+
const { next, cleared: n } = stripModelIdFromInferenceDevices(devices, modelId);
|
|
50941
|
+
if (n === 0) continue;
|
|
50942
|
+
await api.pipelineOrchestrator.setAgentInferenceDevices.mutate({
|
|
50943
|
+
agentNodeId: agent.nodeId,
|
|
50944
|
+
inferenceDevices: next
|
|
50945
|
+
});
|
|
50946
|
+
cleared += n;
|
|
50947
|
+
}
|
|
50948
|
+
return cleared;
|
|
50949
|
+
}
|
|
50950
|
+
/**
|
|
50951
|
+
* Clear per-camera step overrides that pin `modelId` for `stepId`.
|
|
50952
|
+
* `patch: null` drops the override so the camera inherits the agent default.
|
|
50953
|
+
*/
|
|
50954
|
+
async clearCameraModelPins(modelId, stepId) {
|
|
50955
|
+
const api = this.ctx.api;
|
|
50956
|
+
if (!api) return 0;
|
|
50957
|
+
const devices = await api.deviceManager.listAll.query({ projection: "slim" });
|
|
50958
|
+
let cleared = 0;
|
|
50959
|
+
for (const device of devices) {
|
|
50960
|
+
const overrides = await api.pipelineOrchestrator.getCameraStepOverrides.query({ deviceId: device.id });
|
|
50961
|
+
if (overrides === null) continue;
|
|
50962
|
+
for (const [agentNodeId, byDeviceKey] of Object.entries(overrides)) for (const [deviceKey, byStep] of Object.entries(byDeviceKey)) {
|
|
50963
|
+
if (byStep[stepId]?.modelId !== modelId) continue;
|
|
50964
|
+
await api.pipelineOrchestrator.setCameraStepOverride.mutate({
|
|
50965
|
+
deviceId: device.id,
|
|
50966
|
+
agentNodeId,
|
|
50967
|
+
deviceKey,
|
|
50968
|
+
addonId: stepId,
|
|
50969
|
+
patch: null
|
|
50970
|
+
});
|
|
50971
|
+
cleared += 1;
|
|
50972
|
+
}
|
|
50973
|
+
}
|
|
50974
|
+
return cleared;
|
|
50493
50975
|
}
|
|
50494
50976
|
async listCustomModels() {
|
|
50495
50977
|
return this.state_.get();
|
|
50496
50978
|
}
|
|
50979
|
+
/**
|
|
50980
|
+
* Self-heal: drop availability rows for modelIds no longer in the Library
|
|
50981
|
+
* registry. Import and Library both trigger this so an orphan cannot keep
|
|
50982
|
+
* Sources stuck on "imported" after a registry wipe.
|
|
50983
|
+
*/
|
|
50984
|
+
async pruneOrphanAvailability_() {
|
|
50985
|
+
try {
|
|
50986
|
+
const [avail, models] = await Promise.all([this.availability_.get(), this.state_.get()]);
|
|
50987
|
+
const { next, pruned } = pruneOrphanAvailability(avail, new Set(models.map((m) => m.entry.id)));
|
|
50988
|
+
if (pruned.length === 0) return;
|
|
50989
|
+
await this.availability_.update(() => next);
|
|
50990
|
+
this.ctx.logger.info("Pruned orphan model availability", { meta: { pruned } });
|
|
50991
|
+
} catch (err) {
|
|
50992
|
+
this.ctx.logger.warn("pruneOrphanAvailability failed (continuing)", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
50993
|
+
}
|
|
50994
|
+
}
|
|
50497
50995
|
};
|
|
50498
50996
|
//#endregion
|
|
50499
50997
|
export { ModelStudioAddon, modelStudioActions as t };
|