@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
|
@@ -31,29 +31,6 @@ node_os = __toESM(node_os);
|
|
|
31
31
|
let node_path = require("node:path");
|
|
32
32
|
node_path = __toESM(node_path);
|
|
33
33
|
let _camstack_system_addon_utils = require("@camstack/system/addon-utils");
|
|
34
|
-
//#region src/scrypted/scrypted-download-policy.ts
|
|
35
|
-
/**
|
|
36
|
-
* Strict per-hop allow-list for Scrypted catalog downloads.
|
|
37
|
-
*
|
|
38
|
-
* Hosts are the ones observed from the pinned revision
|
|
39
|
-
* `187402b20a3be0a7fab9bceee21d857ed6ae0064` — not a generic CDN wildcard.
|
|
40
|
-
*/
|
|
41
|
-
var SCRYPTED_DOWNLOAD_REDIRECT_HOSTS = ["huggingface.co", "us.aws.cdn.hf.co"];
|
|
42
|
-
var LOCAL_HOSTS = new Set([
|
|
43
|
-
"localhost",
|
|
44
|
-
"127.0.0.1",
|
|
45
|
-
"::1",
|
|
46
|
-
"0.0.0.0"
|
|
47
|
-
]);
|
|
48
|
-
var IPV4_RE = /^(?:\d{1,3}\.){3}\d{1,3}$/;
|
|
49
|
-
function assertScryptedDownloadUrl(url, _hop) {
|
|
50
|
-
if (url.protocol !== "https:") throw new Error(`http downgrade refused: ${url.protocol}`);
|
|
51
|
-
if (url.username || url.password) throw new Error("userinfo not allowed on download URL");
|
|
52
|
-
const host = url.hostname.toLowerCase();
|
|
53
|
-
if (LOCAL_HOSTS.has(host) || IPV4_RE.test(host) || host.includes(":")) throw new Error(`host not allowed: ${host}`);
|
|
54
|
-
if (!SCRYPTED_DOWNLOAD_REDIRECT_HOSTS.includes(host)) throw new Error(`host not allowed: ${host}`);
|
|
55
|
-
}
|
|
56
|
-
//#endregion
|
|
57
34
|
//#region src/scrypted/scrypted-yolo-manifest-data.json
|
|
58
35
|
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}]");
|
|
59
36
|
//#endregion
|
|
@@ -150,44 +127,55 @@ function packageDirectory(files) {
|
|
|
150
127
|
return assertScryptedRelativePath(manifest.relativePath.replace(/\/Manifest\.json$/, ""));
|
|
151
128
|
}
|
|
152
129
|
function buildModel(spec) {
|
|
130
|
+
const tier = spec.family.slice(-1);
|
|
131
|
+
const int8 = spec.precision === "int8" || spec.precision === "uint8" || spec.precision === "integer_quant" || spec.precision === "full_integer_quant" || spec.precision === "edgetpu";
|
|
153
132
|
const group = {
|
|
154
133
|
family: "yolov9",
|
|
155
|
-
tier
|
|
134
|
+
tier,
|
|
156
135
|
resolution: 320,
|
|
157
|
-
...spec.variant === "relu_test" ? { optimization: "
|
|
158
|
-
...
|
|
136
|
+
...spec.variant === "relu_test" ? { optimization: "test" } : {},
|
|
137
|
+
...int8 ? { precision: "int8" } : {}
|
|
138
|
+
};
|
|
139
|
+
const sizeLabel = {
|
|
140
|
+
t: "Tiny",
|
|
141
|
+
s: "Small",
|
|
142
|
+
m: "Medium",
|
|
143
|
+
c: "Compact"
|
|
144
|
+
};
|
|
145
|
+
const canonicalId = `scrypted-yolov9${tier}-${spec.variant}${int8 ? "-int8" : ""}-320`;
|
|
146
|
+
const nameParts = [`YOLOv9 ${sizeLabel[tier] ?? tier.toUpperCase()} @320`];
|
|
147
|
+
if (spec.variant === "relu_test") nameParts.push("(Test)");
|
|
148
|
+
if (int8) nameParts.push("INT8");
|
|
149
|
+
const name = nameParts.join(" ");
|
|
150
|
+
const description = `${name} — imported from the local Scrypted YOLO catalog.`;
|
|
151
|
+
if (spec.format === "coreml") return {
|
|
152
|
+
id: canonicalId,
|
|
153
|
+
name,
|
|
154
|
+
description,
|
|
155
|
+
formats: { coreml: {
|
|
156
|
+
url: buildScryptedResolveUrl(packageDirectory(spec.files)),
|
|
157
|
+
sizeMB: spec.sizeMB,
|
|
158
|
+
isDirectory: true,
|
|
159
|
+
files: [...MLPACKAGE_INNER_FILES],
|
|
160
|
+
runtimes: ["python"]
|
|
161
|
+
} },
|
|
162
|
+
inputSize: INPUT_SIZE,
|
|
163
|
+
labels: SCRYPTED_LABEL_DEFS,
|
|
164
|
+
preprocessMode: "resize",
|
|
165
|
+
classMap: SCRYPTED_IDENTITY_CLASS_MAP,
|
|
166
|
+
group,
|
|
167
|
+
provider: "scrypted",
|
|
168
|
+
license: SCRYPTED_YOLOV9_UNKNOWN
|
|
159
169
|
};
|
|
160
|
-
if (spec.format === "coreml") {
|
|
161
|
-
const dir = packageDirectory(spec.files);
|
|
162
|
-
return {
|
|
163
|
-
id: spec.id,
|
|
164
|
-
name: spec.label,
|
|
165
|
-
description: `${spec.label} — imported from the local Scrypted YOLO catalog.`,
|
|
166
|
-
formats: { coreml: {
|
|
167
|
-
url: buildScryptedResolveUrl(dir),
|
|
168
|
-
sizeMB: spec.sizeMB,
|
|
169
|
-
isDirectory: true,
|
|
170
|
-
files: [...MLPACKAGE_INNER_FILES],
|
|
171
|
-
runtimes: ["python"]
|
|
172
|
-
} },
|
|
173
|
-
inputSize: INPUT_SIZE,
|
|
174
|
-
labels: SCRYPTED_LABEL_DEFS,
|
|
175
|
-
preprocessMode: "resize",
|
|
176
|
-
classMap: SCRYPTED_IDENTITY_CLASS_MAP,
|
|
177
|
-
group,
|
|
178
|
-
provider: "scrypted",
|
|
179
|
-
license: SCRYPTED_YOLOV9_UNKNOWN
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
170
|
if (spec.format === "openvino") {
|
|
183
171
|
const xml = spec.files.find((file) => file.relativePath.endsWith(".xml"));
|
|
184
172
|
const bin = spec.files.find((file) => file.relativePath.endsWith(".bin"));
|
|
185
173
|
if (!xml || !bin) throw new Error(`OpenVINO entry ${spec.id} is missing xml/bin`);
|
|
186
174
|
const binName = bin.relativePath.split("/").pop() ?? "best-converted.bin";
|
|
187
175
|
return {
|
|
188
|
-
id:
|
|
189
|
-
name
|
|
190
|
-
description
|
|
176
|
+
id: canonicalId,
|
|
177
|
+
name,
|
|
178
|
+
description,
|
|
191
179
|
formats: { openvino: {
|
|
192
180
|
url: buildScryptedResolveUrl(xml.relativePath),
|
|
193
181
|
sizeMB: spec.sizeMB,
|
|
@@ -206,9 +194,9 @@ function buildModel(spec) {
|
|
|
206
194
|
const main = spec.files[0];
|
|
207
195
|
if (!main) throw new Error(`Entry ${spec.id} has no files`);
|
|
208
196
|
return {
|
|
209
|
-
id:
|
|
210
|
-
name
|
|
211
|
-
description
|
|
197
|
+
id: canonicalId,
|
|
198
|
+
name,
|
|
199
|
+
description,
|
|
212
200
|
formats: { [spec.format]: {
|
|
213
201
|
url: buildScryptedResolveUrl(main.relativePath),
|
|
214
202
|
sizeMB: spec.sizeMB,
|
|
@@ -220,7 +208,8 @@ function buildModel(spec) {
|
|
|
220
208
|
classMap: SCRYPTED_IDENTITY_CLASS_MAP,
|
|
221
209
|
group,
|
|
222
210
|
provider: "scrypted",
|
|
223
|
-
license: SCRYPTED_YOLOV9_UNKNOWN
|
|
211
|
+
license: SCRYPTED_YOLOV9_UNKNOWN,
|
|
212
|
+
...spec.format === "tflite" ? { postprocessor: "yolo-split-dfl" } : {}
|
|
224
213
|
};
|
|
225
214
|
}
|
|
226
215
|
function freezeEntry(spec) {
|
|
@@ -386,188 +375,22 @@ async function fetchScryptedRemoteTree(opts) {
|
|
|
386
375
|
* Downloads use the pinned host/repo/revision, a bounded staging dir, LFS /
|
|
387
376
|
* size / checksum checks, and atomic finalize with cleanup on every failure.
|
|
388
377
|
*/
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
hash.update(lines.join("\n"));
|
|
395
|
-
return hash.digest("hex");
|
|
396
|
-
}
|
|
397
|
-
var LFS_POINTER_PREFIX = "version https://git-lfs.github.com/spec/v1";
|
|
398
|
-
var SIZE_TOLERANCE = .01;
|
|
399
|
-
function isLfsPointer(filePath) {
|
|
400
|
-
const fd = node_fs.openSync(filePath, "r");
|
|
401
|
-
try {
|
|
402
|
-
const buf = Buffer.alloc(128);
|
|
403
|
-
const n = node_fs.readSync(fd, buf, 0, buf.length, 0);
|
|
404
|
-
return buf.subarray(0, n).toString("utf8").startsWith(LFS_POINTER_PREFIX);
|
|
405
|
-
} finally {
|
|
406
|
-
node_fs.closeSync(fd);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
async function hashFileSha256(filePath) {
|
|
410
|
-
const hash = (0, node_crypto.createHash)("sha256");
|
|
411
|
-
await new Promise((resolve, reject) => {
|
|
412
|
-
node_fs.createReadStream(filePath).on("data", (chunk) => hash.update(chunk)).on("end", () => resolve()).on("error", reject);
|
|
413
|
-
});
|
|
414
|
-
return hash.digest("hex");
|
|
415
|
-
}
|
|
416
|
-
function assertInside(root, dest) {
|
|
417
|
-
const rootResolved = node_path.resolve(root);
|
|
418
|
-
const destResolved = node_path.resolve(dest);
|
|
419
|
-
const prefix = rootResolved.endsWith(node_path.sep) ? rootResolved : `${rootResolved}${node_path.sep}`;
|
|
420
|
-
if (destResolved !== rootResolved && !destResolved.startsWith(prefix)) throw new Error(`Refusing to write outside the destination root: ${dest}`);
|
|
421
|
-
return destResolved;
|
|
422
|
-
}
|
|
423
|
-
function rejectSymlink(filePath) {
|
|
424
|
-
try {
|
|
425
|
-
if (node_fs.lstatSync(filePath).isSymbolicLink()) throw new Error(`Refusing symlink at ${filePath}`);
|
|
426
|
-
} catch (err) {
|
|
427
|
-
if (err.code === "ENOENT") return;
|
|
428
|
-
throw err;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
function sizeOutOfTolerance(actual, declared) {
|
|
432
|
-
if (declared <= 0) return true;
|
|
433
|
-
return Math.abs(actual - declared) / declared > SIZE_TOLERANCE;
|
|
434
|
-
}
|
|
435
|
-
function localBasename(entry) {
|
|
436
|
-
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`;
|
|
437
|
-
if (entry.format === "openvino") return `${entry.id}.xml`;
|
|
438
|
-
if (entry.format === "tflite") return `${entry.id}.tflite`;
|
|
439
|
-
return entry.files[0]?.relativePath.split("/").pop() ?? `${entry.id}.onnx`;
|
|
440
|
-
}
|
|
441
|
-
function buildImportedDescriptor(entry, localName) {
|
|
442
|
-
const catalog = {
|
|
443
|
-
...entry.model,
|
|
444
|
-
classMap: SCRYPTED_IDENTITY_CLASS_MAP,
|
|
445
|
-
formats: { ...entry.model.formats }
|
|
446
|
-
};
|
|
447
|
-
const format = entry.format;
|
|
448
|
-
const previous = catalog.formats[format];
|
|
449
|
-
if (format === "coreml") catalog.formats = {
|
|
450
|
-
...catalog.formats,
|
|
451
|
-
coreml: {
|
|
452
|
-
url: `camstack-local://${entry.id}/${localName}`,
|
|
453
|
-
sizeMB: previous?.sizeMB ?? entry.files.reduce((sum, file) => sum + file.size, 0) / 1e6,
|
|
454
|
-
isDirectory: true,
|
|
455
|
-
files: previous?.files ?? [
|
|
456
|
-
"Manifest.json",
|
|
457
|
-
"Data/com.apple.CoreML/model.mlmodel",
|
|
458
|
-
"Data/com.apple.CoreML/weights/weight.bin"
|
|
459
|
-
],
|
|
460
|
-
runtimes: ["python"]
|
|
461
|
-
}
|
|
462
|
-
};
|
|
463
|
-
else if (format === "openvino") catalog.formats = {
|
|
464
|
-
...catalog.formats,
|
|
465
|
-
openvino: {
|
|
466
|
-
url: `camstack-local://${entry.id}/${localName}`,
|
|
467
|
-
sizeMB: previous?.sizeMB ?? entry.files.reduce((sum, file) => sum + file.size, 0) / 1e6,
|
|
468
|
-
files: [`${entry.id}.bin`],
|
|
469
|
-
runtimes: ["python"]
|
|
470
|
-
}
|
|
471
|
-
};
|
|
472
|
-
else catalog.formats = {
|
|
473
|
-
...catalog.formats,
|
|
474
|
-
[format]: {
|
|
475
|
-
url: `camstack-local://${entry.id}/${localName}`,
|
|
476
|
-
sizeMB: previous?.sizeMB ?? entry.files.reduce((sum, file) => sum + file.size, 0) / 1e6,
|
|
477
|
-
...previous?.runtimes !== void 0 ? { runtimes: previous.runtimes } : {}
|
|
478
|
-
}
|
|
479
|
-
};
|
|
378
|
+
/**
|
|
379
|
+
* Library registration descriptor: keeps manifesto remote URLs (HF resolve).
|
|
380
|
+
* Files land on a node only via Distribute / downloadModel — not hub staging.
|
|
381
|
+
*/
|
|
382
|
+
function buildScryptedRegistryDescriptor(entry) {
|
|
480
383
|
return {
|
|
481
384
|
stepId: "object-detection",
|
|
482
|
-
entry:
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const format = entry.format;
|
|
490
|
-
if (!descriptor.entry.formats[format]) throw new Error(`resolveScryptedImportedFormat: descriptor for "${manifestId}" is missing manifesto format ${format}`);
|
|
491
|
-
return format;
|
|
492
|
-
}
|
|
493
|
-
async function verifyScryptedStagedFile(destPath, file, deps) {
|
|
494
|
-
const maxBytes = deps.maxFileBytes ?? 268435456;
|
|
495
|
-
if (file.size > maxBytes) throw new Error(`Declared size ${file.size} exceeds the ${maxBytes}-byte limit`);
|
|
496
|
-
if (!node_fs.existsSync(destPath)) throw new Error(`missing downloaded file for ${file.relativePath}`);
|
|
497
|
-
rejectSymlink(destPath);
|
|
498
|
-
const actual = node_fs.statSync(destPath).size;
|
|
499
|
-
if (actual > maxBytes) throw new Error(`Downloaded ${actual} bytes exceeds the ${maxBytes}-byte limit`);
|
|
500
|
-
if (isLfsPointer(destPath)) throw new Error(`Downloaded an LFS pointer instead of the artifact (${file.relativePath})`);
|
|
501
|
-
if (sizeOutOfTolerance(actual, file.size)) throw new Error(`Downloaded size ${actual} is outside tolerance of declared ${file.size} for ${file.relativePath}`);
|
|
502
|
-
const expected = assertScryptedFileSha256(file.sha256, file.relativePath);
|
|
503
|
-
if (await (deps.fileSha256 ?? hashFileSha256)(destPath) !== expected) throw new Error(`sha256 checksum mismatch for ${file.relativePath}`);
|
|
504
|
-
}
|
|
505
|
-
async function importScryptedCatalogModel(input, deps) {
|
|
506
|
-
const entry = (deps.resolveEntry ?? getScryptedManifestEntry)(input.manifestId);
|
|
507
|
-
if (!entry) throw new Error(`importScryptedCatalogModel: unknown manifest id "${input.manifestId}"`);
|
|
508
|
-
for (const file of entry.files) assertScryptedFileSha256(file.sha256, file.relativePath);
|
|
509
|
-
const drift = normalizeScryptedDrift(await deps.driftFor(entry.id));
|
|
510
|
-
if (drift.status !== "ok") throw new Error(`importScryptedCatalogModel: entry "${entry.id}" is blocked by drift (${drift.status}${drift.details ? `: ${drift.details}` : ""})`);
|
|
511
|
-
node_fs.mkdirSync(deps.stagingDir, { recursive: true });
|
|
512
|
-
node_fs.mkdirSync(deps.modelsDir, { recursive: true });
|
|
513
|
-
const stagingRoot = node_path.join(deps.stagingDir, `imp-${(0, node_crypto.randomUUID)()}`);
|
|
514
|
-
node_fs.mkdirSync(stagingRoot, { recursive: true });
|
|
515
|
-
try {
|
|
516
|
-
const staged = [];
|
|
517
|
-
for (const file of entry.files) {
|
|
518
|
-
const relative = assertScryptedRelativePath(file.relativePath);
|
|
519
|
-
const dest = assertInside(stagingRoot, node_path.join(stagingRoot, ...relative.split("/")));
|
|
520
|
-
const url = buildScryptedResolveUrl(relative);
|
|
521
|
-
await deps.downloadFile(url, dest);
|
|
522
|
-
await verifyScryptedStagedFile(dest, file, deps);
|
|
523
|
-
staged.push({
|
|
524
|
-
file,
|
|
525
|
-
dest
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
if (entry.format === "coreml") {
|
|
529
|
-
const hasManifest = staged.some((row) => row.file.relativePath.endsWith("Manifest.json"));
|
|
530
|
-
const hasModel = staged.some((row) => row.file.relativePath.endsWith("model.mlmodel"));
|
|
531
|
-
const hasWeight = staged.some((row) => row.file.relativePath.endsWith("weight.bin"));
|
|
532
|
-
if (!hasManifest || !hasModel || !hasWeight || staged.length < 3) throw new Error("CoreML payload is incomplete — Manifest.json, model and weights are required");
|
|
533
|
-
}
|
|
534
|
-
if (entry.format === "openvino") {
|
|
535
|
-
const hasXml = staged.some((row) => row.file.relativePath.endsWith(".xml"));
|
|
536
|
-
const hasBin = staged.some((row) => row.file.relativePath.endsWith(".bin"));
|
|
537
|
-
if (!hasXml || !hasBin) throw new Error("OpenVINO payload is incomplete — xml and bin siblings are required");
|
|
538
|
-
}
|
|
539
|
-
const localName = localBasename(entry);
|
|
540
|
-
const finalMain = assertInside(deps.modelsDir, node_path.join(deps.modelsDir, localName));
|
|
541
|
-
rejectSymlink(finalMain);
|
|
542
|
-
if (entry.format === "coreml") {
|
|
543
|
-
const pkgRel = staged.find((row) => row.file.relativePath.endsWith("Manifest.json"))?.file.relativePath.replace(/\/Manifest\.json$/, "");
|
|
544
|
-
if (!pkgRel) throw new Error("CoreML payload is incomplete — Manifest.json missing");
|
|
545
|
-
const stagedPkg = node_path.join(stagingRoot, ...pkgRel.split("/"));
|
|
546
|
-
node_fs.rmSync(finalMain, {
|
|
547
|
-
recursive: true,
|
|
548
|
-
force: true
|
|
549
|
-
});
|
|
550
|
-
node_fs.renameSync(stagedPkg, finalMain);
|
|
551
|
-
} else if (entry.format === "openvino") {
|
|
552
|
-
const xml = staged.find((row) => row.file.relativePath.endsWith(".xml"));
|
|
553
|
-
const bin = staged.find((row) => row.file.relativePath.endsWith(".bin"));
|
|
554
|
-
if (!xml || !bin) throw new Error("OpenVINO payload is incomplete");
|
|
555
|
-
const xmlDest = finalMain;
|
|
556
|
-
const binDest = assertInside(deps.modelsDir, node_path.join(deps.modelsDir, `${entry.id}.bin`));
|
|
557
|
-
node_fs.renameSync(xml.dest, xmlDest);
|
|
558
|
-
node_fs.renameSync(bin.dest, binDest);
|
|
559
|
-
} else {
|
|
560
|
-
const only = staged[0];
|
|
561
|
-
if (!only) throw new Error("payload is incomplete");
|
|
562
|
-
node_fs.renameSync(only.dest, finalMain);
|
|
385
|
+
entry: {
|
|
386
|
+
...entry.model,
|
|
387
|
+
classMap: SCRYPTED_IDENTITY_CLASS_MAP,
|
|
388
|
+
formats: { ...entry.model.formats },
|
|
389
|
+
name: entry.model.name,
|
|
390
|
+
description: entry.model.description,
|
|
391
|
+
provider: "scrypted"
|
|
563
392
|
}
|
|
564
|
-
|
|
565
|
-
} finally {
|
|
566
|
-
node_fs.rmSync(stagingRoot, {
|
|
567
|
-
recursive: true,
|
|
568
|
-
force: true
|
|
569
|
-
});
|
|
570
|
-
}
|
|
393
|
+
};
|
|
571
394
|
}
|
|
572
395
|
//#endregion
|
|
573
396
|
//#region ../types/dist/event-category-BGmz_oxW.mjs
|
|
@@ -11172,11 +10995,11 @@ var ModelVariantGroupSchema = object({
|
|
|
11172
10995
|
/** Quantization axis. Omit ⇒ the fp32 base build. */
|
|
11173
10996
|
precision: _enum(["fp32", "int8"]).optional(),
|
|
11174
10997
|
/**
|
|
11175
|
-
*
|
|
11176
|
-
*
|
|
11177
|
-
*
|
|
10998
|
+
* Training / latency-variant axis. Omit ⇒ the standard build. `test` marks
|
|
10999
|
+
* an alternate training of the same family (Scrypted `relu_test`); chip
|
|
11000
|
+
* label is "Test".
|
|
11178
11001
|
*/
|
|
11179
|
-
optimization: _enum(["standard", "
|
|
11002
|
+
optimization: _enum(["standard", "test"]).optional(),
|
|
11180
11003
|
/**
|
|
11181
11004
|
* Input-resolution axis (square input side, px). Omit ⇒ the family's native
|
|
11182
11005
|
* resolution (640 for yolo26). Reduced-input builds (320 / 256) are a big,
|
|
@@ -11199,9 +11022,10 @@ var ModelProviderIdSchema = _enum([
|
|
|
11199
11022
|
* are per upstream. A persisted custom-model row written when this field was a
|
|
11200
11023
|
* string still parses — the string is lifted into `{ weights }`.
|
|
11201
11024
|
*
|
|
11202
|
-
* Every licence surface (the
|
|
11203
|
-
*
|
|
11204
|
-
*
|
|
11025
|
+
* Every machine licence surface (`THIRD_PARTY_MODELS.md`, the Hugging Face
|
|
11026
|
+
* model table) is GENERATED from these records by
|
|
11027
|
+
* `scripts/gen-model-licenses.ts` (D661). Operator-facing docs live in
|
|
11028
|
+
* `camstack/docs` → `/docs/detection/models`.
|
|
11205
11029
|
*/
|
|
11206
11030
|
var ModelLicenseSchema = preprocess((value) => typeof value === "string" ? { weights: value } : value, object({
|
|
11207
11031
|
/** SPDX id of the WEIGHTS' terms, a `LicenseRef-*` for non-SPDX terms, or `UNKNOWN`. */
|
|
@@ -14764,7 +14588,7 @@ DeviceType.Camera;
|
|
|
14764
14588
|
* This matters here more than anywhere else in the repo, because every provider
|
|
14765
14589
|
* that knows a device's host knows its PASSWORD too: `{ host, port, username,
|
|
14766
14590
|
* password }` sit in one object on Hikvision, Amcrest, Reolink and ONVIF alike,
|
|
14767
|
-
* and `http://admin:hunter2@192.168.
|
|
14591
|
+
* and `http://admin:hunter2@192.168.60.139/` is a URL a browser accepts. The
|
|
14768
14592
|
* camera's own page will ask for its own login. That is correct, and pre-
|
|
14769
14593
|
* filling it is the operator's business, not ours.
|
|
14770
14594
|
*
|
|
@@ -17754,7 +17578,7 @@ var modelConvertCapability = {
|
|
|
17754
17578
|
}) }
|
|
17755
17579
|
};
|
|
17756
17580
|
method(object({
|
|
17757
|
-
|
|
17581
|
+
targetNodeId: string(),
|
|
17758
17582
|
modelId: string(),
|
|
17759
17583
|
format: _enum(MODEL_FORMATS),
|
|
17760
17584
|
entry: ModelCatalogEntrySchema
|
|
@@ -20220,8 +20044,9 @@ var NcRuleSchema = NcRuleInputSchema.extend({
|
|
|
20220
20044
|
disabledTargetIds: array(string()).default([])
|
|
20221
20045
|
});
|
|
20222
20046
|
/**
|
|
20223
|
-
* The state-summary SECTIONS (D647 / D698): the D637 kind groups that
|
|
20224
|
-
* still-true state. `
|
|
20047
|
+
* The state-summary SECTIONS (D647 / D698 / D708): the D637 kind groups that
|
|
20048
|
+
* report a still-true state. `addons` is the crash-looped addons only and
|
|
20049
|
+
* `addon-updates` the addons that are behind (D708 split them). `backups-exports` and `alarm` are not sections — a completed
|
|
20225
20050
|
* job is not an open state, and the alarm stays instant.
|
|
20226
20051
|
*/
|
|
20227
20052
|
var NcStateSummarySectionIdSchema = _enum([
|
|
@@ -20233,6 +20058,7 @@ var NcStateSummarySectionIdSchema = _enum([
|
|
|
20233
20058
|
"detection-blind",
|
|
20234
20059
|
"nodes",
|
|
20235
20060
|
"addons",
|
|
20061
|
+
"addon-updates",
|
|
20236
20062
|
"updates"
|
|
20237
20063
|
]);
|
|
20238
20064
|
/** One section of a previewed state summary. `unavailable` = its state could not be read. */
|
|
@@ -20713,7 +20539,19 @@ var NcAlarmModeDelaysListSchema = array(object({
|
|
|
20713
20539
|
* When this mode may be armed. Absent / null = always allowed (same as a
|
|
20714
20540
|
* rule with no `schedule`). Evaluated with {@link isScheduleActive}.
|
|
20715
20541
|
*/
|
|
20716
|
-
schedule: NcScheduleSchema.optional()
|
|
20542
|
+
schedule: NcScheduleSchema.optional(),
|
|
20543
|
+
/**
|
|
20544
|
+
* D706: while this mode's ENTRY DELAY runs, do the alarm rules' own pushes go out (as an
|
|
20545
|
+
* intrusion naming the locations)? Absent = false — the entry-delay notification is the
|
|
20546
|
+
* only one, and the matches ride the triggered notification's mosaic (D705).
|
|
20547
|
+
*/
|
|
20548
|
+
notifyDuringEntryDelay: boolean().optional(),
|
|
20549
|
+
/**
|
|
20550
|
+
* D706: notify "watched again" when a sensor this arm bypassed closes and opens again.
|
|
20551
|
+
* Absent = false (logged, not sent). Even when true it is not sent when that same sensor
|
|
20552
|
+
* is the one that starts the entry delay — the entry-delay notification already names it.
|
|
20553
|
+
*/
|
|
20554
|
+
notifySensorRewatched: boolean().optional()
|
|
20717
20555
|
})).max(8).refine(uniqueModes, { message: "modeDelays: at most one entry per arm mode" });
|
|
20718
20556
|
var NcAlarmModeDelaysEditListSchema = array(object({
|
|
20719
20557
|
mode: AlarmArmModeSchema,
|
|
@@ -20722,7 +20560,11 @@ var NcAlarmModeDelaysEditListSchema = array(object({
|
|
|
20722
20560
|
triggeredDurationSec: NcAlarmTriggeredSecSchema.nullable().optional(),
|
|
20723
20561
|
postTriggeredAction: NcAlarmPostTriggeredActionSchema.nullable().optional(),
|
|
20724
20562
|
/** Set a schedule, or `null` to clear (mode always allowed again). */
|
|
20725
|
-
schedule: NcScheduleSchema.nullable().optional()
|
|
20563
|
+
schedule: NcScheduleSchema.nullable().optional(),
|
|
20564
|
+
/** D706: set the switch, or `null` to clear it (back to the default, false). */
|
|
20565
|
+
notifyDuringEntryDelay: boolean().nullable().optional(),
|
|
20566
|
+
/** D706: set the switch, or `null` to clear it (back to the default, false). */
|
|
20567
|
+
notifySensorRewatched: boolean().nullable().optional()
|
|
20726
20568
|
})).max(8).refine(uniqueModes, { message: "modeDelays: at most one edit per arm mode" });
|
|
20727
20569
|
/** One switch of the alarm table: put ONE device on, or take it off, ONE mode's list. */
|
|
20728
20570
|
var NcAlarmNonBlockingEditSchema = object({
|
|
@@ -21683,39 +21525,19 @@ Object.freeze(Object.fromEntries([{
|
|
|
21683
21525
|
label: "Face recognition model",
|
|
21684
21526
|
defaultModelId: "auraface-r100",
|
|
21685
21527
|
indexName: "the enrolled face gallery",
|
|
21686
|
-
options: [
|
|
21687
|
-
|
|
21688
|
-
|
|
21689
|
-
|
|
21690
|
-
},
|
|
21691
|
-
{
|
|
21692
|
-
id: "arcface-r100",
|
|
21693
|
-
label: "ArcFace (ResNet34)"
|
|
21694
|
-
},
|
|
21695
|
-
{
|
|
21696
|
-
id: "inception-resnet-v1",
|
|
21697
|
-
label: "Inception ResNet V1"
|
|
21698
|
-
}
|
|
21699
|
-
]
|
|
21528
|
+
options: [{
|
|
21529
|
+
id: "auraface-r100",
|
|
21530
|
+
label: "AuraFace R100"
|
|
21531
|
+
}]
|
|
21700
21532
|
}, {
|
|
21701
21533
|
stepId: "clip-embedding",
|
|
21702
21534
|
label: "Semantic search model",
|
|
21703
|
-
defaultModelId: "
|
|
21535
|
+
defaultModelId: "siglip2-b16-224",
|
|
21704
21536
|
indexName: "the object semantic-search index",
|
|
21705
|
-
options: [
|
|
21706
|
-
|
|
21707
|
-
|
|
21708
|
-
|
|
21709
|
-
},
|
|
21710
|
-
{
|
|
21711
|
-
id: "mobileclip-s2",
|
|
21712
|
-
label: "MobileCLIP S2"
|
|
21713
|
-
},
|
|
21714
|
-
{
|
|
21715
|
-
id: "siglip2-b16-224",
|
|
21716
|
-
label: "SigLIP2 B/16 (Apache-2.0)"
|
|
21717
|
-
}
|
|
21718
|
-
]
|
|
21537
|
+
options: [{
|
|
21538
|
+
id: "siglip2-b16-224",
|
|
21539
|
+
label: "SigLIP2 B/16 (Apache-2.0)"
|
|
21540
|
+
}]
|
|
21719
21541
|
}].map((s) => [s.stepId, s.defaultModelId])));
|
|
21720
21542
|
var CLUSTER_STEP_SETTING_FIELDS = [{
|
|
21721
21543
|
stepId: "face-embedding",
|
|
@@ -24563,6 +24385,22 @@ var MotionSourceEnum = _enum([
|
|
|
24563
24385
|
"device-activity"
|
|
24564
24386
|
]);
|
|
24565
24387
|
/**
|
|
24388
|
+
* What opened an on-motion detection SESSION: the motion source that passed the
|
|
24389
|
+
* camera's `motionSources` gate, and what raised it (the camera itself, or a
|
|
24390
|
+
* sensor the operator attached — D672). `sensorName` is the attached sensor's
|
|
24391
|
+
* device name as the orchestrator's camera-sensors resolution last read it;
|
|
24392
|
+
* absent when unknown or when the camera itself triggered.
|
|
24393
|
+
*
|
|
24394
|
+
* Carried on the session attach ONLY so the runner's `DetectionPhaseTransition`
|
|
24395
|
+
* can say what started the session (D672 rule 11, amended 2026-10-06). The
|
|
24396
|
+
* runner never decides anything on it.
|
|
24397
|
+
*/
|
|
24398
|
+
var SessionTriggerSchema = object({
|
|
24399
|
+
source: MotionSourceEnum,
|
|
24400
|
+
triggeredBy: MotionTriggeredBySchema,
|
|
24401
|
+
sensorName: string().optional()
|
|
24402
|
+
});
|
|
24403
|
+
/**
|
|
24566
24404
|
* List of motion sources active on a camera. Empty array is valid:
|
|
24567
24405
|
* "no source" — happens for battery cams without firmware motion when
|
|
24568
24406
|
* the operator hasn't opted into the SW analyzer (would hold the
|
|
@@ -24814,6 +24652,14 @@ var RunnerCameraConfigSchema = object({
|
|
|
24814
24652
|
*/
|
|
24815
24653
|
motionSources: MotionSourcesSchema.default(["analyzer"]),
|
|
24816
24654
|
/**
|
|
24655
|
+
* What opened this attach, on a SESSION attach only (see
|
|
24656
|
+
* {@link SessionTriggerSchema}). OPTIONAL with no default: absent on a
|
|
24657
|
+
* standing attach, an occupancy burst, a re-dispatch with no motion event
|
|
24658
|
+
* behind it, and every attach from an orchestrator older than the field — the
|
|
24659
|
+
* runner then names no source on the phase event.
|
|
24660
|
+
*/
|
|
24661
|
+
sessionTrigger: SessionTriggerSchema.optional(),
|
|
24662
|
+
/**
|
|
24817
24663
|
* WHICH root detector runs for this camera. The detection counterpart of
|
|
24818
24664
|
* {@link motionSources}, same shape and same discipline: a per-camera list
|
|
24819
24665
|
* of sources the system understands, vendor-agnostic, owned here.
|
|
@@ -31037,7 +30883,7 @@ var LocalInterfaceSchema = object({
|
|
|
31037
30883
|
name: string(),
|
|
31038
30884
|
family: _enum(["IPv4", "IPv6"]),
|
|
31039
30885
|
address: string(),
|
|
31040
|
-
/** CIDR shorthand (e.g. `192.168.
|
|
30886
|
+
/** CIDR shorthand (e.g. `192.168.10.10/24`). Empty when OS doesn't expose. */
|
|
31041
30887
|
cidr: string(),
|
|
31042
30888
|
/** OS netmask. Stable across calls so the SDK can hash on it. */
|
|
31043
30889
|
netmask: string(),
|
|
@@ -31091,7 +30937,8 @@ var ConnectionEndpointSchema = object({
|
|
|
31091
30937
|
"lan-ipv4",
|
|
31092
30938
|
"lan-ipv6",
|
|
31093
30939
|
"public",
|
|
31094
|
-
"loopback"
|
|
30940
|
+
"loopback",
|
|
30941
|
+
"app-link"
|
|
31095
30942
|
]),
|
|
31096
30943
|
/**
|
|
31097
30944
|
* Originating-interface kind — `lan`/`wifi`/`docker`/`vpn`/`other`/
|
|
@@ -31160,15 +31007,27 @@ var GetConnectionEndpointsResultSchema = object({
|
|
|
31160
31007
|
localPort: AdvertisedLocalPortSchema
|
|
31161
31008
|
});
|
|
31162
31009
|
/**
|
|
31163
|
-
* The
|
|
31164
|
-
*
|
|
31165
|
-
*
|
|
31010
|
+
* The address flagged for notification links. `baseUrl: null` = AUTO, which
|
|
31011
|
+
* since D709 is the CamStack app link; `resolved` reports the effective value,
|
|
31012
|
+
* so the UI can show it either way.
|
|
31166
31013
|
*/
|
|
31167
31014
|
var NotificationEndpointSchema = object({
|
|
31168
31015
|
/** The operator's explicit choice, or null for AUTO. */
|
|
31169
31016
|
baseUrl: string().nullable(),
|
|
31170
|
-
/**
|
|
31171
|
-
|
|
31017
|
+
/**
|
|
31018
|
+
* The effective flagged address: the explicit choice, else (AUTO) the
|
|
31019
|
+
* CamStack app link `https://camstack.zentik.app` (D709). A pre-D709 hub
|
|
31020
|
+
* answered the public tunnel / mesh here, or null.
|
|
31021
|
+
*/
|
|
31022
|
+
resolved: string().nullable(),
|
|
31023
|
+
/**
|
|
31024
|
+
* The hub origin an app link carries as `hub=` (D709): the hub's best
|
|
31025
|
+
* public HTTPS address — the tunnel, then the mesh (Tailscale MagicDNS) —
|
|
31026
|
+
* picked from this cap's own palette by `pickAppLinkHub`. `null` when the
|
|
31027
|
+
* hub advertises neither; the app link is then minted without `hub=`.
|
|
31028
|
+
* Absent on a pre-D709 hub.
|
|
31029
|
+
*/
|
|
31030
|
+
appLinkHub: string().nullable()
|
|
31172
31031
|
});
|
|
31173
31032
|
/**
|
|
31174
31033
|
* The URLs the SDK / viewer should race for API access. `baseUrls` empty =
|
|
@@ -31222,7 +31081,11 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
|
|
|
31222
31081
|
* Pass `'https'` when the caller is itself loaded over HTTPS
|
|
31223
31082
|
* to avoid mixed-content blocks in the browser. The public
|
|
31224
31083
|
* tunnel always emits `https://` regardless. */
|
|
31225
|
-
scheme: _enum(["http", "https"]).optional()
|
|
31084
|
+
scheme: _enum(["http", "https"]).optional(),
|
|
31085
|
+
/** Append the CamStack app link entry (`kind: 'app-link'`, D709) —
|
|
31086
|
+
* an address notification links can be flagged with, not a hub
|
|
31087
|
+
* route. Default `false`: racers and rankers never see it. */
|
|
31088
|
+
includeAppLink: boolean().optional()
|
|
31226
31089
|
}), 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, {
|
|
31227
31090
|
kind: "mutation",
|
|
31228
31091
|
auth: "admin"
|
|
@@ -31483,7 +31346,7 @@ var MeshPeerSchema = object({
|
|
|
31483
31346
|
tags: array(string()).readonly(),
|
|
31484
31347
|
/** True when the peer advertises itself as an exit-node candidate. */
|
|
31485
31348
|
exitNodeOption: boolean(),
|
|
31486
|
-
/** Subnet routes the peer advertises (e.g. `["192.168.
|
|
31349
|
+
/** Subnet routes the peer advertises (e.g. `["192.168.10.0/24"]`). */
|
|
31487
31350
|
advertisedRoutes: array(string()).readonly(),
|
|
31488
31351
|
/** Owner account login (e.g. `foo@example.com`). Null for tagged peers. */
|
|
31489
31352
|
userLogin: string().nullable(),
|
|
@@ -32427,7 +32290,13 @@ var OsdStatusSchema = object({ overlays: array(object({
|
|
|
32427
32290
|
*/
|
|
32428
32291
|
availablePositions: array(OsdPositionEnum).readonly(),
|
|
32429
32292
|
/** Firmware does not allow editing the `text` field (e.g. timestamp). */
|
|
32430
|
-
readOnly: boolean().optional()
|
|
32293
|
+
readOnly: boolean().optional(),
|
|
32294
|
+
/**
|
|
32295
|
+
* Firmware text ceiling for this overlay. Absent = no known limit.
|
|
32296
|
+
* The OSD manager truncates here on every write — never stores its own
|
|
32297
|
+
* copy on the binding.
|
|
32298
|
+
*/
|
|
32299
|
+
maxCharacters: number().int().positive().optional()
|
|
32431
32300
|
})).readonly() });
|
|
32432
32301
|
var OsdOverlayPatchSchema = object({
|
|
32433
32302
|
enabled: boolean().optional(),
|
|
@@ -32478,7 +32347,9 @@ var OsdTemplateBindingSchema = object({
|
|
|
32478
32347
|
valuePath: string().min(1).max(64),
|
|
32479
32348
|
/** Convert TO this unit at render (`tryConvertUnit`). Absent = source unit. */
|
|
32480
32349
|
displayUnit: string().max(16).optional(),
|
|
32481
|
-
maxDecimals: number().int().min(0).max(4).default(1)
|
|
32350
|
+
maxDecimals: number().int().min(0).max(4).default(1),
|
|
32351
|
+
/** Raw value → display text (same shape as slot binding). Empty string hides. */
|
|
32352
|
+
valueMap: record(string(), string()).optional()
|
|
32482
32353
|
});
|
|
32483
32354
|
/**
|
|
32484
32355
|
* A named template: bindings (alias → leaf field) + a parser that
|
|
@@ -32528,18 +32399,11 @@ var OsdSlotBindingSchema = object({
|
|
|
32528
32399
|
source: OsdSourceSchema,
|
|
32529
32400
|
/** `${value}` and `${unit}` are substituted; every occurrence. */
|
|
32530
32401
|
template: string().max(96).default("${value}"),
|
|
32531
|
-
/** Truncate with an ellipsis past this length. Absent = no limit. */
|
|
32532
|
-
maxCharacters: number().int().min(4).max(64).optional(),
|
|
32533
32402
|
/**
|
|
32534
32403
|
* Decimal places for a numeric value. `0` yields an integer — the
|
|
32535
32404
|
* documented workaround for firmwares that reject `.` in overlay text.
|
|
32536
32405
|
*/
|
|
32537
32406
|
maxDecimals: number().int().min(0).max(4).default(1),
|
|
32538
|
-
/**
|
|
32539
|
-
* Appended via `${unit}` when no unit conversion produces a label.
|
|
32540
|
-
* Prefer `source.displayUnit` for converted device-state values.
|
|
32541
|
-
*/
|
|
32542
|
-
unitLabel: string().max(8).optional(),
|
|
32543
32407
|
/** Raw value → display text, e.g. `{"true":"MOTION","false":""}`. */
|
|
32544
32408
|
valueMap: record(string(), string()).optional(),
|
|
32545
32409
|
/** Time windows in which the slot is shown. Absent = always. */
|
|
@@ -32548,9 +32412,7 @@ var OsdSlotBindingSchema = object({
|
|
|
32548
32412
|
* Display gate, in the notification centre's condition vocabulary.
|
|
32549
32413
|
* Only the keys reported by `getConditionSupport` are accepted.
|
|
32550
32414
|
*/
|
|
32551
|
-
conditions: NcConditionsSchema.optional()
|
|
32552
|
-
/** Rendered when the gate is closed or the value unreadable. Empty = hide. */
|
|
32553
|
-
fallbackText: string().max(64).default("")
|
|
32415
|
+
conditions: NcConditionsSchema.optional()
|
|
32554
32416
|
});
|
|
32555
32417
|
/** One camera slot, as the operator sees it: firmware truth + our binding. */
|
|
32556
32418
|
var OsdSlotViewSchema = object({
|
|
@@ -48227,12 +48089,14 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
|
|
|
48227
48089
|
{
|
|
48228
48090
|
id: "addons",
|
|
48229
48091
|
family: "system",
|
|
48230
|
-
label: "Addons",
|
|
48231
|
-
kinds: [
|
|
48232
|
-
|
|
48233
|
-
|
|
48234
|
-
|
|
48235
|
-
|
|
48092
|
+
label: "Addons stopped",
|
|
48093
|
+
kinds: ["addon-crash-loop"]
|
|
48094
|
+
},
|
|
48095
|
+
{
|
|
48096
|
+
id: "addon-updates",
|
|
48097
|
+
family: "system",
|
|
48098
|
+
label: "Addon updates",
|
|
48099
|
+
kinds: ["addon-update-available", "addon-updated"]
|
|
48236
48100
|
},
|
|
48237
48101
|
{
|
|
48238
48102
|
id: "updates",
|
|
@@ -48392,13 +48256,15 @@ var STATE_SUMMARY_SECTION_VARS = [
|
|
|
48392
48256
|
},
|
|
48393
48257
|
{
|
|
48394
48258
|
name: "summaryAddons",
|
|
48395
|
-
label: "Summary: addons",
|
|
48396
|
-
example: "
|
|
48397
|
-
kinds: [
|
|
48398
|
-
|
|
48399
|
-
|
|
48400
|
-
|
|
48401
|
-
|
|
48259
|
+
label: "Summary: addons stopped",
|
|
48260
|
+
example: "hub — recorder stopped after repeated crashes",
|
|
48261
|
+
kinds: ["addon-crash-loop"]
|
|
48262
|
+
},
|
|
48263
|
+
{
|
|
48264
|
+
name: "summaryAddonUpdates",
|
|
48265
|
+
label: "Summary: addon updates",
|
|
48266
|
+
example: "hub:\n• @camstack/addon-recorder 1.2.3 → 1.2.4",
|
|
48267
|
+
kinds: ["addon-update-available", "addon-updated"]
|
|
48402
48268
|
},
|
|
48403
48269
|
{
|
|
48404
48270
|
name: "summaryUpdates",
|
|
@@ -48482,6 +48348,7 @@ var STATE_SUMMARY_VARS = [
|
|
|
48482
48348
|
[...RULE_BODY_ONLY],
|
|
48483
48349
|
[...RULE_TEXT],
|
|
48484
48350
|
([...RULE_TEXT], [...DEVICE_EVENTS]),
|
|
48351
|
+
([...RULE_TEXT], [...COMBINED_ALARM]),
|
|
48485
48352
|
([...RULE_TEXT], [...DEVICE_EVENTS]),
|
|
48486
48353
|
([...RULE_TEXT], [...BATTERY, ...CONSUMABLE]),
|
|
48487
48354
|
([...RULE_TEXT], [...CONSUMABLE]),
|
|
@@ -48522,6 +48389,8 @@ var STATE_SUMMARY_VARS = [
|
|
|
48522
48389
|
[...RULE_TEXT],
|
|
48523
48390
|
[...RULE_TEXT],
|
|
48524
48391
|
[...RULE_TEXT],
|
|
48392
|
+
[...RULE_TEXT],
|
|
48393
|
+
[...RULE_TEXT],
|
|
48525
48394
|
...STATE_SUMMARY_VARS
|
|
48526
48395
|
];
|
|
48527
48396
|
new Set([
|
|
@@ -49102,7 +48971,9 @@ async function convertModel(input, deps) {
|
|
|
49102
48971
|
var ConvertProgressSchema = object({
|
|
49103
48972
|
phase: string(),
|
|
49104
48973
|
pct: number().optional(),
|
|
49105
|
-
detail: string().optional()
|
|
48974
|
+
detail: string().optional(),
|
|
48975
|
+
/** Raw process line (stderr) — UI log pane, not a phase step. */
|
|
48976
|
+
log: string().optional()
|
|
49106
48977
|
});
|
|
49107
48978
|
var ConvertResultLineSchema = object({ result: object({ artifacts: array(ConvertArtifactSchema) }) });
|
|
49108
48979
|
function buildEntry(metadata, artifacts) {
|
|
@@ -49171,29 +49042,57 @@ function buildModelConvertProvider(deps) {
|
|
|
49171
49042
|
//#endregion
|
|
49172
49043
|
//#region src/model-registry.ts
|
|
49173
49044
|
/**
|
|
49174
|
-
*
|
|
49175
|
-
*
|
|
49045
|
+
* Record that `modelId` now has `format` on `nodeId`. Merges into the node's
|
|
49046
|
+
* formats map — never drops a sibling format on the same node.
|
|
49176
49047
|
*/
|
|
49177
|
-
function
|
|
49048
|
+
function upsertAvailabilityFormat(map, modelId, nodeId, format, presence) {
|
|
49049
|
+
const prevNode = map[modelId]?.[nodeId] ?? { formats: {} };
|
|
49178
49050
|
return {
|
|
49179
49051
|
...map,
|
|
49180
49052
|
[modelId]: {
|
|
49181
49053
|
...map[modelId],
|
|
49182
|
-
[nodeId]:
|
|
49054
|
+
[nodeId]: { formats: {
|
|
49055
|
+
...prevNode.formats,
|
|
49056
|
+
[format]: presence
|
|
49057
|
+
} }
|
|
49183
49058
|
}
|
|
49184
49059
|
};
|
|
49185
49060
|
}
|
|
49186
49061
|
/**
|
|
49062
|
+
* @deprecated Prefer {@link upsertAvailabilityFormat}. Kept for call sites that
|
|
49063
|
+
* still pass a single-format legacy shape — folds into multi-format.
|
|
49064
|
+
*/
|
|
49065
|
+
function upsertAvailability(map, modelId, nodeId, entry) {
|
|
49066
|
+
return upsertAvailabilityFormat(map, modelId, nodeId, entry.format, {
|
|
49067
|
+
sha256: entry.sha256,
|
|
49068
|
+
bytes: entry.bytes,
|
|
49069
|
+
at: entry.at
|
|
49070
|
+
});
|
|
49071
|
+
}
|
|
49072
|
+
/**
|
|
49187
49073
|
* Pure registry operations over the custom-model list. Kept side-effect-free
|
|
49188
49074
|
* so they unit-test without a live `BaseAddon` context — the addon class is a
|
|
49189
49075
|
* thin durable-state wrapper around these.
|
|
49190
49076
|
*/
|
|
49191
49077
|
/**
|
|
49192
|
-
* Insert or replace a descriptor keyed by `entry.id
|
|
49193
|
-
*
|
|
49078
|
+
* Insert or replace a descriptor keyed by `entry.id`. When the id already
|
|
49079
|
+
* exists, **formats are merged** (Scrypted multi-format accumulate) — sibling
|
|
49080
|
+
* formats on the prior entry are never dropped. Returns a new array.
|
|
49194
49081
|
*/
|
|
49195
49082
|
function upsertCustomModel(list, descriptor) {
|
|
49196
|
-
|
|
49083
|
+
const prev = list.find((m) => m.entry.id === descriptor.entry.id);
|
|
49084
|
+
const merged = prev === void 0 ? descriptor : {
|
|
49085
|
+
stepId: descriptor.stepId,
|
|
49086
|
+
entry: {
|
|
49087
|
+
...prev.entry,
|
|
49088
|
+
...descriptor.entry,
|
|
49089
|
+
formats: {
|
|
49090
|
+
...prev.entry.formats,
|
|
49091
|
+
...descriptor.entry.formats
|
|
49092
|
+
}
|
|
49093
|
+
}
|
|
49094
|
+
};
|
|
49095
|
+
return [...list.filter((m) => m.entry.id !== descriptor.entry.id), merged];
|
|
49197
49096
|
}
|
|
49198
49097
|
/** Drop the descriptor with the given `entry.id`. Reports whether anything changed. */
|
|
49199
49098
|
function removeCustomModel(list, modelId) {
|
|
@@ -49203,6 +49102,114 @@ function removeCustomModel(list, modelId) {
|
|
|
49203
49102
|
removed: next.length !== list.length
|
|
49204
49103
|
};
|
|
49205
49104
|
}
|
|
49105
|
+
/** Drop every node entry for `modelId` from the availability map. */
|
|
49106
|
+
function clearAvailability(map, modelId) {
|
|
49107
|
+
if (map[modelId] === void 0) return {
|
|
49108
|
+
next: map,
|
|
49109
|
+
hadEntry: false
|
|
49110
|
+
};
|
|
49111
|
+
const next = { ...map };
|
|
49112
|
+
delete next[modelId];
|
|
49113
|
+
return {
|
|
49114
|
+
next,
|
|
49115
|
+
hadEntry: true
|
|
49116
|
+
};
|
|
49117
|
+
}
|
|
49118
|
+
/**
|
|
49119
|
+
* Drop one `(modelId, nodeId, format)` from availability. Empty nodes and empty
|
|
49120
|
+
* model rows are pruned so Library "On nodes" does not keep a ghost chip.
|
|
49121
|
+
*/
|
|
49122
|
+
function clearAvailabilityFormat(map, modelId, nodeId, format) {
|
|
49123
|
+
const perNode = map[modelId];
|
|
49124
|
+
if (perNode === void 0) return {
|
|
49125
|
+
next: map,
|
|
49126
|
+
cleared: false
|
|
49127
|
+
};
|
|
49128
|
+
const node = perNode[nodeId];
|
|
49129
|
+
if (node === void 0 || node.formats[format] === void 0) return {
|
|
49130
|
+
next: map,
|
|
49131
|
+
cleared: false
|
|
49132
|
+
};
|
|
49133
|
+
const formats = { ...node.formats };
|
|
49134
|
+
delete formats[format];
|
|
49135
|
+
const nextNodes = { ...perNode };
|
|
49136
|
+
if (Object.keys(formats).length === 0) delete nextNodes[nodeId];
|
|
49137
|
+
else nextNodes[nodeId] = { formats };
|
|
49138
|
+
if (Object.keys(nextNodes).length === 0) {
|
|
49139
|
+
const next = { ...map };
|
|
49140
|
+
delete next[modelId];
|
|
49141
|
+
return {
|
|
49142
|
+
next,
|
|
49143
|
+
cleared: true
|
|
49144
|
+
};
|
|
49145
|
+
}
|
|
49146
|
+
return {
|
|
49147
|
+
next: {
|
|
49148
|
+
...map,
|
|
49149
|
+
[modelId]: nextNodes
|
|
49150
|
+
},
|
|
49151
|
+
cleared: true
|
|
49152
|
+
};
|
|
49153
|
+
}
|
|
49154
|
+
/**
|
|
49155
|
+
* Drop availability rows whose modelId is not in the Library registry.
|
|
49156
|
+
* Orphans left Sources showing "imported" after a registry-only wipe.
|
|
49157
|
+
*/
|
|
49158
|
+
function pruneOrphanAvailability(map, registeredIds) {
|
|
49159
|
+
const pruned = [];
|
|
49160
|
+
let next = null;
|
|
49161
|
+
for (const modelId of Object.keys(map)) {
|
|
49162
|
+
if (registeredIds.has(modelId)) continue;
|
|
49163
|
+
if (next === null) next = { ...map };
|
|
49164
|
+
delete next[modelId];
|
|
49165
|
+
pruned.push(modelId);
|
|
49166
|
+
}
|
|
49167
|
+
return {
|
|
49168
|
+
next: next ?? map,
|
|
49169
|
+
pruned
|
|
49170
|
+
};
|
|
49171
|
+
}
|
|
49172
|
+
function stripModelIdFromInferenceDevices(devices, modelId) {
|
|
49173
|
+
let cleared = 0;
|
|
49174
|
+
const next = {};
|
|
49175
|
+
for (const [key, device] of Object.entries(devices)) {
|
|
49176
|
+
const stepsIn = device.steps;
|
|
49177
|
+
if (stepsIn === void 0) {
|
|
49178
|
+
next[key] = {
|
|
49179
|
+
enabled: device.enabled,
|
|
49180
|
+
...device.weight !== void 0 ? { weight: device.weight } : {},
|
|
49181
|
+
...device.maxSessions !== void 0 ? { maxSessions: device.maxSessions } : {}
|
|
49182
|
+
};
|
|
49183
|
+
continue;
|
|
49184
|
+
}
|
|
49185
|
+
const stepsOut = {};
|
|
49186
|
+
for (const [stepId, step] of Object.entries(stepsIn)) {
|
|
49187
|
+
if (step.modelId === modelId) {
|
|
49188
|
+
cleared += 1;
|
|
49189
|
+
const kept = {};
|
|
49190
|
+
if (step.settings !== void 0) kept.settings = { ...step.settings };
|
|
49191
|
+
if (step.jumpDeviceKey !== void 0) kept.jumpDeviceKey = step.jumpDeviceKey;
|
|
49192
|
+
if (Object.keys(kept).length > 0) stepsOut[stepId] = kept;
|
|
49193
|
+
continue;
|
|
49194
|
+
}
|
|
49195
|
+
stepsOut[stepId] = {
|
|
49196
|
+
...step.modelId !== void 0 ? { modelId: step.modelId } : {},
|
|
49197
|
+
...step.settings !== void 0 ? { settings: { ...step.settings } } : {},
|
|
49198
|
+
...step.jumpDeviceKey !== void 0 ? { jumpDeviceKey: step.jumpDeviceKey } : {}
|
|
49199
|
+
};
|
|
49200
|
+
}
|
|
49201
|
+
next[key] = {
|
|
49202
|
+
enabled: device.enabled,
|
|
49203
|
+
...device.weight !== void 0 ? { weight: device.weight } : {},
|
|
49204
|
+
...device.maxSessions !== void 0 ? { maxSessions: device.maxSessions } : {},
|
|
49205
|
+
...Object.keys(stepsOut).length > 0 ? { steps: stepsOut } : {}
|
|
49206
|
+
};
|
|
49207
|
+
}
|
|
49208
|
+
return {
|
|
49209
|
+
next,
|
|
49210
|
+
cleared
|
|
49211
|
+
};
|
|
49212
|
+
}
|
|
49206
49213
|
//#endregion
|
|
49207
49214
|
//#region src/frigate/frigate-import.ts
|
|
49208
49215
|
/**
|
|
@@ -49438,6 +49445,64 @@ async function runPlusConversions(deps, args) {
|
|
|
49438
49445
|
return outcomes;
|
|
49439
49446
|
}
|
|
49440
49447
|
//#endregion
|
|
49448
|
+
//#region src/ensure-formats.ts
|
|
49449
|
+
var ENGINE_FORMATS = ["openvino", "coreml"];
|
|
49450
|
+
function missingEngineFormats(entry) {
|
|
49451
|
+
return ENGINE_FORMATS.filter((f) => entry.formats[f] === void 0);
|
|
49452
|
+
}
|
|
49453
|
+
async function ensureEngineFormats(entry, deps, opts = {}) {
|
|
49454
|
+
if (entry.formats.onnx === void 0) return {
|
|
49455
|
+
entry,
|
|
49456
|
+
conversions: []
|
|
49457
|
+
};
|
|
49458
|
+
const missing = (opts.targetFormats ?? ENGINE_FORMATS).filter((f) => entry.formats[f] === void 0);
|
|
49459
|
+
if (missing.length === 0) return {
|
|
49460
|
+
entry,
|
|
49461
|
+
conversions: []
|
|
49462
|
+
};
|
|
49463
|
+
return {
|
|
49464
|
+
entry,
|
|
49465
|
+
conversions: await runPlusConversions(deps, {
|
|
49466
|
+
entry,
|
|
49467
|
+
targetFormats: missing
|
|
49468
|
+
})
|
|
49469
|
+
};
|
|
49470
|
+
}
|
|
49471
|
+
/**
|
|
49472
|
+
* Ensure OV/CoreML from ONNX when missing, then push every declared format to
|
|
49473
|
+
* every agent (hub is skipped — it already hosts imports).
|
|
49474
|
+
*/
|
|
49475
|
+
async function ensureFormatsThenDistribute(modelId, deps) {
|
|
49476
|
+
const initial = await deps.getEntry();
|
|
49477
|
+
if (initial === void 0) throw new Error(`ensureFormatsThenDistribute: model "${modelId}" is not registered`);
|
|
49478
|
+
let conversions = [];
|
|
49479
|
+
if (initial.formats.onnx !== void 0 && missingEngineFormats(initial).length > 0) conversions = (await ensureEngineFormats(initial, deps)).conversions;
|
|
49480
|
+
const entry = await deps.getEntry() ?? initial;
|
|
49481
|
+
const formats = Object.keys(entry.formats).filter((f) => entry.formats[f] !== void 0);
|
|
49482
|
+
const agents = await deps.listAgentNodeIds();
|
|
49483
|
+
const distributed = [];
|
|
49484
|
+
for (const nodeId of agents) for (const format of formats) try {
|
|
49485
|
+
const res = await deps.distribute(modelId, nodeId, format);
|
|
49486
|
+
distributed.push({
|
|
49487
|
+
nodeId,
|
|
49488
|
+
format,
|
|
49489
|
+
ok: true,
|
|
49490
|
+
bytes: res.bytes
|
|
49491
|
+
});
|
|
49492
|
+
} catch (err) {
|
|
49493
|
+
distributed.push({
|
|
49494
|
+
nodeId,
|
|
49495
|
+
format,
|
|
49496
|
+
ok: false,
|
|
49497
|
+
error: err instanceof Error ? err.message : String(err)
|
|
49498
|
+
});
|
|
49499
|
+
}
|
|
49500
|
+
return {
|
|
49501
|
+
conversions,
|
|
49502
|
+
distributed
|
|
49503
|
+
};
|
|
49504
|
+
}
|
|
49505
|
+
//#endregion
|
|
49441
49506
|
//#region src/frigate/frigate-plus.ts
|
|
49442
49507
|
var PLUS_API_HOST = "https://api.frigate.video";
|
|
49443
49508
|
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}$/;
|
|
@@ -49541,6 +49606,12 @@ var PLUS_TYPE_MAP = {
|
|
|
49541
49606
|
postprocessor: "ssd",
|
|
49542
49607
|
extension: ".tflite"
|
|
49543
49608
|
},
|
|
49609
|
+
/** List API uses `ssd` for MobileDet rows. */
|
|
49610
|
+
ssd: {
|
|
49611
|
+
format: "tflite",
|
|
49612
|
+
postprocessor: "ssd",
|
|
49613
|
+
extension: ".tflite"
|
|
49614
|
+
},
|
|
49544
49615
|
yolonas: {
|
|
49545
49616
|
format: "onnx",
|
|
49546
49617
|
postprocessor: "yolonas",
|
|
@@ -49549,6 +49620,10 @@ var PLUS_TYPE_MAP = {
|
|
|
49549
49620
|
yolov9: {
|
|
49550
49621
|
format: "onnx",
|
|
49551
49622
|
extension: ".onnx"
|
|
49623
|
+
},
|
|
49624
|
+
"yolo-generic": {
|
|
49625
|
+
format: "onnx",
|
|
49626
|
+
extension: ".onnx"
|
|
49552
49627
|
}
|
|
49553
49628
|
};
|
|
49554
49629
|
function plusTypeTraits(type) {
|
|
@@ -49573,7 +49648,16 @@ function orderedLabels(labelMap) {
|
|
|
49573
49648
|
* Distribute.
|
|
49574
49649
|
*/
|
|
49575
49650
|
function plusInfoToDescriptor(info, opts) {
|
|
49576
|
-
const
|
|
49651
|
+
const base = plusTypeTraits(info.type);
|
|
49652
|
+
const traits = opts.format === "tflite" ? {
|
|
49653
|
+
format: "tflite",
|
|
49654
|
+
extension: ".tflite",
|
|
49655
|
+
...info.type === "ssd" || info.type === "mobiledet" ? { postprocessor: "ssd" } : {}
|
|
49656
|
+
} : opts.format === "onnx" ? {
|
|
49657
|
+
format: "onnx",
|
|
49658
|
+
extension: ".onnx",
|
|
49659
|
+
...base.postprocessor !== void 0 ? { postprocessor: base.postprocessor } : {}
|
|
49660
|
+
} : base;
|
|
49577
49661
|
const labels = orderedLabels(info.labelMap);
|
|
49578
49662
|
return {
|
|
49579
49663
|
stepId: "object-detection",
|
|
@@ -49691,15 +49775,33 @@ var ModelUploadStore = class {
|
|
|
49691
49775
|
* only the read-array `listModels` (auto-concatenated across providers); the
|
|
49692
49776
|
* mutations live here so the cap stays a clean collection.
|
|
49693
49777
|
*/
|
|
49694
|
-
/** One
|
|
49695
|
-
var
|
|
49696
|
-
format: _enum(MODEL_FORMATS),
|
|
49778
|
+
/** One format's presence on a node. */
|
|
49779
|
+
var ModelFormatPresenceSchema = object({
|
|
49697
49780
|
sha256: string(),
|
|
49698
49781
|
bytes: number(),
|
|
49699
49782
|
at: number()
|
|
49700
49783
|
});
|
|
49701
|
-
/**
|
|
49784
|
+
/**
|
|
49785
|
+
* Per-node availability — multi-format. Legacy durable rows were
|
|
49786
|
+
* `{ format, sha256, bytes, at }`; preprocess folds them into `formats`.
|
|
49787
|
+
*/
|
|
49788
|
+
var ModelNodeAvailabilitySchema = preprocess((raw) => {
|
|
49789
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return raw;
|
|
49790
|
+
const rec = raw;
|
|
49791
|
+
if (typeof rec.formats === "object" && rec.formats !== null && !Array.isArray(rec.formats)) return { formats: rec.formats };
|
|
49792
|
+
if (typeof rec.format === "string") return { formats: { [rec.format]: {
|
|
49793
|
+
sha256: typeof rec.sha256 === "string" ? rec.sha256 : "",
|
|
49794
|
+
bytes: typeof rec.bytes === "number" ? rec.bytes : 0,
|
|
49795
|
+
at: typeof rec.at === "number" ? rec.at : 0
|
|
49796
|
+
} } };
|
|
49797
|
+
return raw;
|
|
49798
|
+
}, object({ formats: partialRecord(_enum(MODEL_FORMATS), ModelFormatPresenceSchema) }));
|
|
49799
|
+
/** `modelId → nodeId → { formats: { [format]: presence } }`. */
|
|
49702
49800
|
var ModelAvailabilityMapSchema = record(string(), record(string(), ModelNodeAvailabilitySchema));
|
|
49801
|
+
/** Stable Library id for a Frigate+ training (chip id ↔ registry). */
|
|
49802
|
+
function frigatePlusRegistryId(plusModelId) {
|
|
49803
|
+
return `frigate-plus-${plusModelId}`;
|
|
49804
|
+
}
|
|
49703
49805
|
var ScryptedManifestFileSchema = object({
|
|
49704
49806
|
relativePath: string(),
|
|
49705
49807
|
size: number(),
|
|
@@ -49772,8 +49874,75 @@ var FrigatePlusModelSchema = object({
|
|
|
49772
49874
|
height: number().optional()
|
|
49773
49875
|
});
|
|
49774
49876
|
var modelStudioActions = defineCustomActions({
|
|
49775
|
-
registerCustomModel: customAction(CustomModelDescriptorSchema, object({
|
|
49776
|
-
|
|
49877
|
+
registerCustomModel: customAction(CustomModelDescriptorSchema, object({
|
|
49878
|
+
ok: literal(true),
|
|
49879
|
+
conversions: array(object({
|
|
49880
|
+
format: _enum(["openvino", "coreml"]),
|
|
49881
|
+
ok: boolean(),
|
|
49882
|
+
nodeId: string().optional(),
|
|
49883
|
+
error: string().optional()
|
|
49884
|
+
})).readonly()
|
|
49885
|
+
}), { kind: "mutation" }),
|
|
49886
|
+
/**
|
|
49887
|
+
* Ensure OV/CoreML from ONNX when missing, then distribute every declared
|
|
49888
|
+
* format to every online agent (hub skipped).
|
|
49889
|
+
*/
|
|
49890
|
+
ensureFormatsThenDistribute: customAction(object({ modelId: string() }), object({
|
|
49891
|
+
conversions: array(object({
|
|
49892
|
+
format: _enum(["openvino", "coreml"]),
|
|
49893
|
+
ok: boolean(),
|
|
49894
|
+
nodeId: string().optional(),
|
|
49895
|
+
error: string().optional()
|
|
49896
|
+
})).readonly(),
|
|
49897
|
+
distributed: array(object({
|
|
49898
|
+
nodeId: string(),
|
|
49899
|
+
format: _enum(MODEL_FORMATS),
|
|
49900
|
+
ok: boolean(),
|
|
49901
|
+
error: string().optional(),
|
|
49902
|
+
bytes: number().optional()
|
|
49903
|
+
})).readonly()
|
|
49904
|
+
}), { kind: "mutation" }),
|
|
49905
|
+
/**
|
|
49906
|
+
* Convert missing OpenVINO / CoreML from an already-registered ONNX entry
|
|
49907
|
+
* (Frigate+ / Custom per-format Import). Only the requested formats run.
|
|
49908
|
+
*/
|
|
49909
|
+
ensureModelFormats: customAction(object({
|
|
49910
|
+
modelId: string(),
|
|
49911
|
+
formats: array(_enum(["openvino", "coreml"])).min(1).readonly()
|
|
49912
|
+
}), object({ conversions: array(object({
|
|
49913
|
+
format: _enum(["openvino", "coreml"]),
|
|
49914
|
+
ok: boolean(),
|
|
49915
|
+
nodeId: string().optional(),
|
|
49916
|
+
error: string().optional()
|
|
49917
|
+
})).readonly() }), { kind: "mutation" }),
|
|
49918
|
+
removeCustomModel: customAction(object({ modelId: string() }), object({
|
|
49919
|
+
removed: boolean(),
|
|
49920
|
+
deletedNodes: array(string()).readonly(),
|
|
49921
|
+
clearedPins: number().int().nonnegative()
|
|
49922
|
+
}), { kind: "mutation" }),
|
|
49923
|
+
/**
|
|
49924
|
+
* Delete one format of a Library model from one node. Keeps the registry row
|
|
49925
|
+
* and sibling formats/nodes; clears only that availability chip.
|
|
49926
|
+
*/
|
|
49927
|
+
removeModelFromNode: customAction(object({
|
|
49928
|
+
modelId: string(),
|
|
49929
|
+
nodeId: string(),
|
|
49930
|
+
format: _enum(MODEL_FORMATS)
|
|
49931
|
+
}), object({
|
|
49932
|
+
deleted: boolean(),
|
|
49933
|
+
clearedAvailability: boolean()
|
|
49934
|
+
}), { kind: "mutation" }),
|
|
49935
|
+
/**
|
|
49936
|
+
* Wipe one CamStack seed format from a node's modelsDir (Installed Remove).
|
|
49937
|
+
* Routes via `nodePin` — `pipelineExecutor.deleteModel` has no `nodeId` in
|
|
49938
|
+
* its input schema, so a bare tRPC mutate always hit the hub.
|
|
49939
|
+
*/
|
|
49940
|
+
deleteSeedFromNode: customAction(object({
|
|
49941
|
+
addonId: string(),
|
|
49942
|
+
modelId: string(),
|
|
49943
|
+
format: _enum(MODEL_FORMATS),
|
|
49944
|
+
nodeId: string()
|
|
49945
|
+
}), object({ success: literal(true) }), { kind: "mutation" }),
|
|
49777
49946
|
listCustomModels: customAction(_void(), array(CustomModelDescriptorSchema).readonly()),
|
|
49778
49947
|
/**
|
|
49779
49948
|
* Distribute a registered model's FORMAT (resident on the hub) to a target
|
|
@@ -49813,17 +49982,16 @@ var modelStudioActions = defineCustomActions({
|
|
|
49813
49982
|
}), { kind: "mutation" }),
|
|
49814
49983
|
abortModelUpload: customAction(object({ uploadId: string() }), object({ ok: literal(true) }), { kind: "mutation" }),
|
|
49815
49984
|
/**
|
|
49816
|
-
* Curated PUBLIC Frigate model catalog (D214 addendum)
|
|
49817
|
-
*
|
|
49818
|
-
*
|
|
49819
|
-
* the hub's modelsDir and registers the descriptor with the public URL
|
|
49820
|
-
* (other nodes download on demand — no Distribute needed).
|
|
49985
|
+
* Curated PUBLIC Frigate model catalog (D214 addendum). Import registers the
|
|
49986
|
+
* descriptor with its public URL (Library only). Optional `targetNodeId`
|
|
49987
|
+
* downloads on that node — no hub staging disk.
|
|
49821
49988
|
*/
|
|
49822
49989
|
listFrigateCatalog: customAction(_void(), array(FrigateCatalogEntrySchema).readonly()),
|
|
49823
49990
|
importFrigateCatalogModel: customAction(object({
|
|
49824
49991
|
catalogId: string(),
|
|
49825
49992
|
modelId: string().optional(),
|
|
49826
|
-
name: string().optional()
|
|
49993
|
+
name: string().optional(),
|
|
49994
|
+
targetNodeId: string().optional()
|
|
49827
49995
|
}), object({
|
|
49828
49996
|
ok: literal(true),
|
|
49829
49997
|
modelId: string(),
|
|
@@ -49832,13 +50000,17 @@ var modelStudioActions = defineCustomActions({
|
|
|
49832
50000
|
/**
|
|
49833
50001
|
* Local Scrypted YOLO catalog (Task 7B). Listing is a projection of the
|
|
49834
50002
|
* revision-pinned manifesto plus a read-only HF drift check. Import accepts
|
|
49835
|
-
*
|
|
50003
|
+
* `manifestId` (+ optional land-on-node) — host/repo/revision/path/labels/
|
|
50004
|
+
* checksum stay server-owned.
|
|
49836
50005
|
*/
|
|
49837
50006
|
listScryptedCatalog: customAction(_void(), object({
|
|
49838
50007
|
entries: array(ScryptedManifestEntrySchema).readonly(),
|
|
49839
50008
|
drift: array(ScryptedCatalogDriftSchema).readonly()
|
|
49840
50009
|
})),
|
|
49841
|
-
importScryptedCatalogModel: customAction(object({
|
|
50010
|
+
importScryptedCatalogModel: customAction(object({
|
|
50011
|
+
manifestId: string(),
|
|
50012
|
+
targetNodeId: string().optional()
|
|
50013
|
+
}), CustomModelDescriptorSchema, { kind: "mutation" }),
|
|
49842
50014
|
/**
|
|
49843
50015
|
* Frigate+ personal models. The API key lives in the addon's SETTINGS
|
|
49844
50016
|
* (password field) and never leaves the hub: list / test / import all run
|
|
@@ -49848,9 +50020,9 @@ var modelStudioActions = defineCustomActions({
|
|
|
49848
50020
|
/**
|
|
49849
50021
|
* Nodes whose engine can actually LOAD a tflite artifact (the pool's only
|
|
49850
50022
|
* tflite runtime binds the Coral EdgeTPU delegate — no Coral, no tflite).
|
|
49851
|
-
*
|
|
50023
|
+
* Empty object — `addons.custom` cannot carry zod `void`.
|
|
49852
50024
|
*/
|
|
49853
|
-
tfliteSupport: customAction(
|
|
50025
|
+
tfliteSupport: customAction(object({}).optional(), object({ nodes: array(string()).readonly() })),
|
|
49854
50026
|
frigatePlusStatus: customAction(_void(), object({
|
|
49855
50027
|
configured: boolean(),
|
|
49856
50028
|
keyFormatValid: boolean()
|
|
@@ -49862,8 +50034,21 @@ var modelStudioActions = defineCustomActions({
|
|
|
49862
50034
|
frigatePlusListModels: customAction(_void(), array(FrigatePlusModelSchema).readonly()),
|
|
49863
50035
|
importFrigatePlusModel: customAction(object({
|
|
49864
50036
|
plusModelId: string(),
|
|
49865
|
-
|
|
49866
|
-
|
|
50037
|
+
/** Ignored for identity — registry id is always `frigate-plus-<plusModelId>`. */
|
|
50038
|
+
modelId: string().optional(),
|
|
50039
|
+
name: string().optional(),
|
|
50040
|
+
/**
|
|
50041
|
+
* Native artifact class from the list row (`plusDownloadTraits`). The
|
|
50042
|
+
* model-detail API does not always carry `supportedDetectors`, so the
|
|
50043
|
+
* browser passes what the catalog already knew (fixes ssd/yolo-generic
|
|
50044
|
+
* EdgeTPU rows that `plusTypeTraits` alone would call onnx).
|
|
50045
|
+
*/
|
|
50046
|
+
format: _enum(["onnx", "tflite"]).optional(),
|
|
50047
|
+
/**
|
|
50048
|
+
* Engine formats to convert after the native artifact lands. Empty =
|
|
50049
|
+
* download + register only (per-format Import clicks OV/CoreML later).
|
|
50050
|
+
*/
|
|
50051
|
+
ensureFormats: array(_enum(["openvino", "coreml"])).readonly().optional()
|
|
49867
50052
|
}), object({
|
|
49868
50053
|
ok: literal(true),
|
|
49869
50054
|
modelId: string(),
|
|
@@ -49946,6 +50131,15 @@ function spawnRunConvert(jobPath, pythonPath, onProgress, _sessionId) {
|
|
|
49946
50131
|
});
|
|
49947
50132
|
proc.stderr.on("data", (chunk) => {
|
|
49948
50133
|
stderrChunks.push(chunk);
|
|
50134
|
+
for (const line of chunk.toString().split("\n")) {
|
|
50135
|
+
const trimmed = line.trimEnd();
|
|
50136
|
+
if (trimmed.length === 0) continue;
|
|
50137
|
+
onProgress({
|
|
50138
|
+
phase: "log",
|
|
50139
|
+
log: trimmed,
|
|
50140
|
+
detail: trimmed
|
|
50141
|
+
});
|
|
50142
|
+
}
|
|
49949
50143
|
});
|
|
49950
50144
|
proc.on("close", (code) => {
|
|
49951
50145
|
if (code !== 0) {
|
|
@@ -50035,6 +50229,7 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
|
50035
50229
|
phase: p.phase,
|
|
50036
50230
|
pct: p.pct,
|
|
50037
50231
|
detail: p.detail,
|
|
50232
|
+
log: p.log,
|
|
50038
50233
|
sessionId
|
|
50039
50234
|
}
|
|
50040
50235
|
});
|
|
@@ -50078,11 +50273,21 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
|
50078
50273
|
return result;
|
|
50079
50274
|
},
|
|
50080
50275
|
abortModelUpload: async (input) => (await this.getUploadStore()).abort(input),
|
|
50081
|
-
registerCustomModel: async (input) => this.registerCustomModel(input),
|
|
50276
|
+
registerCustomModel: async (input) => this.registerCustomModel(input, { ensureEngines: true }),
|
|
50277
|
+
ensureFormatsThenDistribute: async (input) => this.ensureFormatsThenDistribute_(input.modelId),
|
|
50278
|
+
ensureModelFormats: async (input) => this.ensureModelFormats_(input.modelId, input.formats),
|
|
50082
50279
|
removeCustomModel: async (input) => this.removeCustomModel(input),
|
|
50083
|
-
|
|
50280
|
+
removeModelFromNode: async (input) => this.removeModelFromNode(input),
|
|
50281
|
+
deleteSeedFromNode: async (input) => this.deleteSeedFromNode(input),
|
|
50282
|
+
listCustomModels: async () => {
|
|
50283
|
+
await this.pruneOrphanAvailability_();
|
|
50284
|
+
return this.listCustomModels();
|
|
50285
|
+
},
|
|
50084
50286
|
distributeModel: async (input) => this.distributeModel(input),
|
|
50085
|
-
listAvailability: async () =>
|
|
50287
|
+
listAvailability: async () => {
|
|
50288
|
+
await this.pruneOrphanAvailability_();
|
|
50289
|
+
return this.availability_.get();
|
|
50290
|
+
},
|
|
50086
50291
|
listFrigateCatalog: async () => FRIGATE_PUBLIC_CATALOG.map((c) => ({
|
|
50087
50292
|
id: c.id,
|
|
50088
50293
|
label: c.label,
|
|
@@ -50227,10 +50432,37 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
|
50227
50432
|
async distributeModel(input) {
|
|
50228
50433
|
const descriptor = (await this.state_.get()).find((m) => m.entry.id === input.modelId);
|
|
50229
50434
|
if (!descriptor) throw new Error(`distributeModel: model "${input.modelId}" is not registered`);
|
|
50435
|
+
const formatEntry = descriptor.entry.formats[input.format];
|
|
50436
|
+
if (formatEntry === void 0) throw new Error(`distributeModel: model "${input.modelId}" has no ${input.format} format in the Library`);
|
|
50230
50437
|
const api = this.ctx.api;
|
|
50231
50438
|
if (!api) throw new Error("distributeModel: ctx.api unavailable");
|
|
50439
|
+
if (/^https?:\/\//i.test(formatEntry.url)) {
|
|
50440
|
+
const downloaded = await api.pipelineExecutor.downloadModel.mutate({
|
|
50441
|
+
modelId: input.modelId,
|
|
50442
|
+
format: input.format,
|
|
50443
|
+
addonId: descriptor.stepId
|
|
50444
|
+
}, nodePin(input.nodeId));
|
|
50445
|
+
const bytes = Math.round(downloaded.sizeMB * 1e6);
|
|
50446
|
+
await this.availability_.update((prev) => upsertAvailability(prev, input.modelId, input.nodeId, {
|
|
50447
|
+
format: input.format,
|
|
50448
|
+
sha256: "",
|
|
50449
|
+
bytes,
|
|
50450
|
+
at: Date.now()
|
|
50451
|
+
}));
|
|
50452
|
+
this.ctx.logger.info("Downloaded custom model on node", { meta: {
|
|
50453
|
+
modelId: input.modelId,
|
|
50454
|
+
nodeId: input.nodeId,
|
|
50455
|
+
format: input.format,
|
|
50456
|
+
bytes
|
|
50457
|
+
} });
|
|
50458
|
+
return {
|
|
50459
|
+
ok: true,
|
|
50460
|
+
sha256: "",
|
|
50461
|
+
bytes
|
|
50462
|
+
};
|
|
50463
|
+
}
|
|
50232
50464
|
const res = await api.modelDistributor.distributeModel.mutate({
|
|
50233
|
-
|
|
50465
|
+
targetNodeId: input.nodeId,
|
|
50234
50466
|
modelId: input.modelId,
|
|
50235
50467
|
format: input.format,
|
|
50236
50468
|
entry: descriptor.entry
|
|
@@ -50309,59 +50541,49 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
|
50309
50541
|
}
|
|
50310
50542
|
}
|
|
50311
50543
|
/**
|
|
50312
|
-
*
|
|
50544
|
+
* Register one manifesto entry by id into the Library. Host/repo/revision
|
|
50545
|
+
* stay server-owned. Artifacts are NOT staged on the hub — optional
|
|
50546
|
+
* `targetNodeId` lands the HF URL on that node via download-on-target.
|
|
50313
50547
|
*/
|
|
50314
50548
|
async importScryptedCatalogFromManifest(input) {
|
|
50315
|
-
const
|
|
50316
|
-
|
|
50317
|
-
|
|
50318
|
-
|
|
50319
|
-
|
|
50320
|
-
|
|
50321
|
-
|
|
50322
|
-
|
|
50323
|
-
|
|
50324
|
-
|
|
50325
|
-
|
|
50326
|
-
|
|
50327
|
-
|
|
50328
|
-
|
|
50329
|
-
|
|
50330
|
-
|
|
50331
|
-
|
|
50332
|
-
|
|
50333
|
-
|
|
50334
|
-
|
|
50335
|
-
manifestId,
|
|
50336
|
-
status: "unavailable",
|
|
50337
|
-
details: err instanceof Error ? err.message : "Hugging Face tree unavailable"
|
|
50338
|
-
};
|
|
50339
|
-
}
|
|
50340
|
-
}
|
|
50341
|
-
});
|
|
50549
|
+
const entry = getScryptedManifestEntry(input.manifestId);
|
|
50550
|
+
if (!entry) throw new Error(`importScryptedCatalogModel: unknown manifest id "${input.manifestId}"`);
|
|
50551
|
+
let drift;
|
|
50552
|
+
try {
|
|
50553
|
+
const tree = await fetchScryptedRemoteTree({ fetchImpl: fetch });
|
|
50554
|
+
const row = compareScryptedManifestDrift(SCRYPTED_YOLO_MANIFEST.filter((e) => e.id === input.manifestId), tree)[0];
|
|
50555
|
+
drift = normalizeScryptedDrift(row ?? {
|
|
50556
|
+
manifestId: input.manifestId,
|
|
50557
|
+
status: "missing",
|
|
50558
|
+
details: "entry missing from manifesto"
|
|
50559
|
+
});
|
|
50560
|
+
} catch (err) {
|
|
50561
|
+
drift = {
|
|
50562
|
+
manifestId: input.manifestId,
|
|
50563
|
+
status: "unavailable",
|
|
50564
|
+
details: err instanceof Error ? err.message : "Hugging Face tree unavailable"
|
|
50565
|
+
};
|
|
50566
|
+
}
|
|
50567
|
+
if (drift.status !== "ok") throw new Error(`importScryptedCatalogModel: entry "${entry.id}" is blocked by drift (${drift.status}${drift.details ? `: ${drift.details}` : ""})`);
|
|
50568
|
+
const descriptor = buildScryptedRegistryDescriptor(entry);
|
|
50342
50569
|
await this.registerCustomModel(descriptor);
|
|
50343
|
-
|
|
50344
|
-
|
|
50345
|
-
|
|
50346
|
-
|
|
50347
|
-
|
|
50348
|
-
|
|
50349
|
-
sha256: scryptedBundleFingerprint(manifestEntry),
|
|
50350
|
-
bytes,
|
|
50351
|
-
at: Date.now()
|
|
50352
|
-
}));
|
|
50353
|
-
this.ctx.logger.info("Imported Scrypted catalog model", { meta: {
|
|
50570
|
+
if (input.targetNodeId !== void 0 && input.targetNodeId.length > 0) await this.distributeModel({
|
|
50571
|
+
modelId: descriptor.entry.id,
|
|
50572
|
+
nodeId: input.targetNodeId,
|
|
50573
|
+
format: entry.format
|
|
50574
|
+
});
|
|
50575
|
+
this.ctx.logger.info("Registered Scrypted catalog model", { meta: {
|
|
50354
50576
|
manifestId: input.manifestId,
|
|
50355
50577
|
modelId: descriptor.entry.id,
|
|
50356
|
-
|
|
50578
|
+
format: entry.format,
|
|
50579
|
+
...input.targetNodeId !== void 0 ? { targetNodeId: input.targetNodeId } : {}
|
|
50357
50580
|
} });
|
|
50358
50581
|
return descriptor;
|
|
50359
50582
|
}
|
|
50360
50583
|
/**
|
|
50361
|
-
*
|
|
50362
|
-
*
|
|
50363
|
-
*
|
|
50364
|
-
* and record hub availability.
|
|
50584
|
+
* Register a curated PUBLIC Frigate catalog model into the Library. The
|
|
50585
|
+
* entry keeps its public URL — no hub staging. Optional `targetNodeId`
|
|
50586
|
+
* downloads on that node (same path as Library → Distribute).
|
|
50365
50587
|
*/
|
|
50366
50588
|
async importFrigateCatalogModel(input) {
|
|
50367
50589
|
const cat = FRIGATE_PUBLIC_CATALOG.find((c) => c.id === input.catalogId);
|
|
@@ -50370,29 +50592,21 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
|
50370
50592
|
...input.modelId !== void 0 ? { modelId: input.modelId } : {},
|
|
50371
50593
|
...input.name !== void 0 ? { name: input.name } : {}
|
|
50372
50594
|
});
|
|
50373
|
-
const modelsDir = await this.resolveModelsDir();
|
|
50374
|
-
const filename = cat.url.split("/").at(-1) ?? `${descriptor.entry.id}.${cat.format}`;
|
|
50375
|
-
const destPath = node_path.default.join(modelsDir, filename);
|
|
50376
|
-
await (0, _camstack_system_addon_utils.downloadFile)(cat.url, destPath);
|
|
50377
|
-
const bytes = (await node_fs.default.promises.stat(destPath)).size;
|
|
50378
|
-
const declaredBytes = cat.sizeMB * 1e6;
|
|
50379
|
-
if (bytes < declaredBytes * .5 || bytes > declaredBytes * 2) {
|
|
50380
|
-
await node_fs.default.promises.rm(destPath, { force: true });
|
|
50381
|
-
throw new Error(`importFrigateCatalogModel: downloaded ${bytes} bytes but the catalog declares ~${Math.round(declaredBytes)} — refusing to register a suspicious artifact`);
|
|
50382
|
-
}
|
|
50383
|
-
const sha256 = await ModelStudioAddon.fileSha256(destPath);
|
|
50384
50595
|
await this.registerCustomModel(descriptor);
|
|
50385
|
-
|
|
50386
|
-
|
|
50387
|
-
|
|
50388
|
-
|
|
50389
|
-
|
|
50390
|
-
|
|
50391
|
-
|
|
50596
|
+
let bytes = Math.round(cat.sizeMB * 1e6);
|
|
50597
|
+
if (input.targetNodeId !== void 0 && input.targetNodeId.length > 0) {
|
|
50598
|
+
const landed = await this.distributeModel({
|
|
50599
|
+
modelId: descriptor.entry.id,
|
|
50600
|
+
nodeId: input.targetNodeId,
|
|
50601
|
+
format: cat.format
|
|
50602
|
+
});
|
|
50603
|
+
bytes = landed.bytes > 0 ? landed.bytes : bytes;
|
|
50604
|
+
}
|
|
50605
|
+
this.ctx.logger.info("Registered Frigate catalog model", { meta: {
|
|
50392
50606
|
catalogId: input.catalogId,
|
|
50393
50607
|
modelId: descriptor.entry.id,
|
|
50394
|
-
|
|
50395
|
-
|
|
50608
|
+
...input.targetNodeId !== void 0 ? { targetNodeId: input.targetNodeId } : {},
|
|
50609
|
+
bytes
|
|
50396
50610
|
} });
|
|
50397
50611
|
return {
|
|
50398
50612
|
ok: true,
|
|
@@ -50408,11 +50622,20 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
|
50408
50622
|
* is hub-local: other nodes get it via Models → Distribute.
|
|
50409
50623
|
*/
|
|
50410
50624
|
async importFrigatePlusModel(input) {
|
|
50411
|
-
|
|
50625
|
+
const modelId = frigatePlusRegistryId(input.plusModelId);
|
|
50412
50626
|
const key = this.config.frigatePlusApiKey ?? "";
|
|
50413
50627
|
const { info, downloadUrl } = await requestPlusModelDownload(plusFetch, key, input.plusModelId);
|
|
50414
|
-
const
|
|
50415
|
-
const
|
|
50628
|
+
const typeTraits = plusTypeTraits(info.type);
|
|
50629
|
+
const traits = input.format === "tflite" ? {
|
|
50630
|
+
format: "tflite",
|
|
50631
|
+
extension: ".tflite",
|
|
50632
|
+
...info.type === "ssd" || info.type === "mobiledet" ? { postprocessor: "ssd" } : {}
|
|
50633
|
+
} : input.format === "onnx" ? {
|
|
50634
|
+
format: "onnx",
|
|
50635
|
+
extension: ".onnx",
|
|
50636
|
+
...typeTraits.postprocessor !== void 0 ? { postprocessor: typeTraits.postprocessor } : {}
|
|
50637
|
+
} : typeTraits;
|
|
50638
|
+
const filename = `${modelId}${traits.extension}`;
|
|
50416
50639
|
const modelsDir = await this.resolveModelsDir();
|
|
50417
50640
|
const destPath = node_path.default.join(modelsDir, filename);
|
|
50418
50641
|
await (0, _camstack_system_addon_utils.downloadFile)(downloadUrl, destPath);
|
|
@@ -50423,13 +50646,14 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
|
50423
50646
|
}
|
|
50424
50647
|
const sha256 = await ModelStudioAddon.fileSha256(destPath);
|
|
50425
50648
|
const descriptor = plusInfoToDescriptor(info, {
|
|
50426
|
-
modelId
|
|
50649
|
+
modelId,
|
|
50427
50650
|
name: input.name?.trim() || `Frigate+ ${info.type} (${input.plusModelId.slice(0, 8)})`,
|
|
50428
50651
|
filename,
|
|
50429
|
-
sizeMB: Number((bytes / 1e6).toFixed(1))
|
|
50652
|
+
sizeMB: Number((bytes / 1e6).toFixed(1)),
|
|
50653
|
+
...input.format !== void 0 ? { format: input.format } : {}
|
|
50430
50654
|
});
|
|
50431
|
-
await this.registerCustomModel(descriptor);
|
|
50432
|
-
await this.availability_.update((prev) => upsertAvailability(prev,
|
|
50655
|
+
await this.registerCustomModel(descriptor, { ensureEngines: false });
|
|
50656
|
+
await this.availability_.update((prev) => upsertAvailability(prev, modelId, "hub", {
|
|
50433
50657
|
format: traits.format,
|
|
50434
50658
|
sha256,
|
|
50435
50659
|
bytes,
|
|
@@ -50437,59 +50661,28 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
|
50437
50661
|
}));
|
|
50438
50662
|
this.ctx.logger.info("Imported Frigate+ model", { meta: {
|
|
50439
50663
|
plusModelId: input.plusModelId,
|
|
50440
|
-
modelId
|
|
50664
|
+
modelId,
|
|
50441
50665
|
type: info.type,
|
|
50442
|
-
bytes
|
|
50666
|
+
bytes,
|
|
50667
|
+
format: traits.format
|
|
50443
50668
|
} });
|
|
50669
|
+
const wanted = input.ensureFormats ?? [];
|
|
50444
50670
|
let conversions = [];
|
|
50445
|
-
if (traits.format === "onnx") {
|
|
50446
|
-
|
|
50447
|
-
|
|
50448
|
-
|
|
50449
|
-
|
|
50450
|
-
|
|
50451
|
-
freshSourceUrl: async () => (await requestPlusModelDownload(plusFetch, key, input.plusModelId)).downloadUrl,
|
|
50452
|
-
upsertEntry: async (entry) => {
|
|
50453
|
-
await this.state_.update((prev) => upsertCustomModel(prev, {
|
|
50454
|
-
stepId: descriptor.stepId,
|
|
50455
|
-
entry
|
|
50456
|
-
}));
|
|
50457
|
-
},
|
|
50458
|
-
markAvailable: async (nId, mId, artifacts) => {
|
|
50459
|
-
await this.availability_.update((prev) => {
|
|
50460
|
-
const seen = /* @__PURE__ */ new Set();
|
|
50461
|
-
let next = prev;
|
|
50462
|
-
for (const artifact of artifacts) {
|
|
50463
|
-
if (seen.has(artifact.format)) continue;
|
|
50464
|
-
seen.add(artifact.format);
|
|
50465
|
-
next = upsertAvailability(next, mId, nId, {
|
|
50466
|
-
format: artifact.format,
|
|
50467
|
-
sha256: "",
|
|
50468
|
-
bytes: Math.round(artifact.sizeMB * 1e6),
|
|
50469
|
-
at: Date.now()
|
|
50470
|
-
});
|
|
50471
|
-
}
|
|
50472
|
-
return next;
|
|
50473
|
-
});
|
|
50474
|
-
}
|
|
50475
|
-
}, {
|
|
50476
|
-
entry: descriptor.entry,
|
|
50477
|
-
targetFormats: ["openvino", "coreml"]
|
|
50478
|
-
});
|
|
50479
|
-
this.ctx.logger.info("Frigate+ import — engine conversions", { meta: {
|
|
50480
|
-
modelId: input.modelId,
|
|
50481
|
-
outcomes: conversions.map((o) => ({ ...o }))
|
|
50482
|
-
} });
|
|
50483
|
-
}
|
|
50671
|
+
if (traits.format === "onnx" && wanted.length > 0) {
|
|
50672
|
+
conversions = (await ensureEngineFormats(descriptor.entry, this.plusConversionDeps(async () => (await requestPlusModelDownload(plusFetch, key, input.plusModelId)).downloadUrl, descriptor.stepId), { targetFormats: wanted })).conversions;
|
|
50673
|
+
this.ctx.logger.info("Frigate+ import — engine conversions", { meta: {
|
|
50674
|
+
modelId,
|
|
50675
|
+
outcomes: conversions.map((o) => ({ ...o }))
|
|
50676
|
+
} });
|
|
50484
50677
|
}
|
|
50485
50678
|
return {
|
|
50486
50679
|
ok: true,
|
|
50487
|
-
modelId
|
|
50680
|
+
modelId,
|
|
50488
50681
|
bytes,
|
|
50489
50682
|
conversions
|
|
50490
50683
|
};
|
|
50491
50684
|
}
|
|
50492
|
-
async registerCustomModel(input) {
|
|
50685
|
+
async registerCustomModel(input, opts = {}) {
|
|
50493
50686
|
if (!input.stepId.trim()) throw new Error("registerCustomModel: stepId is required");
|
|
50494
50687
|
if (!input.entry.id.trim()) throw new Error("registerCustomModel: entry.id is required");
|
|
50495
50688
|
await this.state_.update((prev) => upsertCustomModel(prev, input));
|
|
@@ -50497,9 +50690,252 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
|
50497
50690
|
modelId: input.entry.id,
|
|
50498
50691
|
stepId: input.stepId
|
|
50499
50692
|
} });
|
|
50500
|
-
|
|
50693
|
+
let conversions = [];
|
|
50694
|
+
if (opts.ensureEngines === true && input.entry.formats.onnx !== void 0) {
|
|
50695
|
+
const onnxUrl = input.entry.formats.onnx.url;
|
|
50696
|
+
if (typeof onnxUrl === "string" && onnxUrl.length > 0) {
|
|
50697
|
+
const deps = this.plusConversionDeps(() => Promise.resolve(onnxUrl), input.stepId);
|
|
50698
|
+
conversions = (await ensureEngineFormats(input.entry, deps)).conversions;
|
|
50699
|
+
if (conversions.length > 0) this.ctx.logger.info("Custom model — engine conversions", { meta: {
|
|
50700
|
+
modelId: input.entry.id,
|
|
50701
|
+
outcomes: conversions.map((o) => ({ ...o }))
|
|
50702
|
+
} });
|
|
50703
|
+
}
|
|
50704
|
+
}
|
|
50705
|
+
return {
|
|
50706
|
+
ok: true,
|
|
50707
|
+
conversions
|
|
50708
|
+
};
|
|
50709
|
+
}
|
|
50710
|
+
plusConversionDeps(freshSourceUrl, stepId) {
|
|
50711
|
+
const api = this.ctx.api;
|
|
50712
|
+
return {
|
|
50713
|
+
recommend: (i) => recommendConvertNode(i, this.recommendDeps()),
|
|
50714
|
+
convert: async (convertInput, nId) => {
|
|
50715
|
+
if (!api) throw new Error("modelConvert unavailable");
|
|
50716
|
+
return api.modelConvert.convert.mutate(convertInput, nodePin(nId));
|
|
50717
|
+
},
|
|
50718
|
+
freshSourceUrl,
|
|
50719
|
+
upsertEntry: async (entry) => {
|
|
50720
|
+
await this.state_.update((prev) => upsertCustomModel(prev, {
|
|
50721
|
+
stepId,
|
|
50722
|
+
entry
|
|
50723
|
+
}));
|
|
50724
|
+
},
|
|
50725
|
+
markAvailable: async (nId, mId, artifacts) => {
|
|
50726
|
+
await this.availability_.update((prev) => {
|
|
50727
|
+
const seen = /* @__PURE__ */ new Set();
|
|
50728
|
+
let next = prev;
|
|
50729
|
+
for (const artifact of artifacts) {
|
|
50730
|
+
if (seen.has(artifact.format)) continue;
|
|
50731
|
+
seen.add(artifact.format);
|
|
50732
|
+
next = upsertAvailability(next, mId, nId, {
|
|
50733
|
+
format: artifact.format,
|
|
50734
|
+
sha256: "",
|
|
50735
|
+
bytes: Math.round(artifact.sizeMB * 1e6),
|
|
50736
|
+
at: Date.now()
|
|
50737
|
+
});
|
|
50738
|
+
}
|
|
50739
|
+
return next;
|
|
50740
|
+
});
|
|
50741
|
+
}
|
|
50742
|
+
};
|
|
50743
|
+
}
|
|
50744
|
+
async ensureModelFormats_(modelId, formats) {
|
|
50745
|
+
const row = (await this.state_.get()).find((m) => m.entry.id === modelId);
|
|
50746
|
+
if (row === void 0) throw new Error(`ensureModelFormats: model "${modelId}" is not registered`);
|
|
50747
|
+
const onnxUrl = row.entry.formats.onnx?.url;
|
|
50748
|
+
if (typeof onnxUrl !== "string" || onnxUrl.length === 0) throw new Error(`ensureModelFormats: model "${modelId}" has no ONNX to convert from`);
|
|
50749
|
+
const ensured = await ensureEngineFormats(row.entry, this.plusConversionDeps(async () => {
|
|
50750
|
+
const url = ((await this.state_.get()).find((m) => m.entry.id === modelId)?.entry)?.formats.onnx?.url ?? onnxUrl;
|
|
50751
|
+
if (typeof url !== "string" || url.length === 0) throw new Error(`ensureModelFormats: no ONNX url for "${modelId}"`);
|
|
50752
|
+
return url;
|
|
50753
|
+
}, row.stepId), { targetFormats: formats });
|
|
50754
|
+
this.ctx.logger.info("ensureModelFormats", { meta: {
|
|
50755
|
+
modelId,
|
|
50756
|
+
outcomes: ensured.conversions.map((o) => ({ ...o }))
|
|
50757
|
+
} });
|
|
50758
|
+
return { conversions: ensured.conversions };
|
|
50759
|
+
}
|
|
50760
|
+
async ensureFormatsThenDistribute_(modelId) {
|
|
50761
|
+
const api = this.ctx.api;
|
|
50762
|
+
if (!api) throw new Error("ensureFormatsThenDistribute: ctx.api unavailable");
|
|
50763
|
+
const getEntry = async () => {
|
|
50764
|
+
return (await this.state_.get()).find((m) => m.entry.id === modelId)?.entry;
|
|
50765
|
+
};
|
|
50766
|
+
const entry = await getEntry();
|
|
50767
|
+
if (entry === void 0) throw new Error(`ensureFormatsThenDistribute: model "${modelId}" is not registered`);
|
|
50768
|
+
const onnxUrl = entry.formats.onnx?.url;
|
|
50769
|
+
const stepId = (await this.state_.get()).find((m) => m.entry.id === modelId)?.stepId ?? "object-detection";
|
|
50770
|
+
return ensureFormatsThenDistribute(modelId, {
|
|
50771
|
+
...this.plusConversionDeps(async () => {
|
|
50772
|
+
const url = (await getEntry())?.formats.onnx?.url ?? onnxUrl;
|
|
50773
|
+
if (typeof url !== "string" || url.length === 0) throw new Error(`ensureFormatsThenDistribute: no ONNX url for "${modelId}"`);
|
|
50774
|
+
return url;
|
|
50775
|
+
}, stepId),
|
|
50776
|
+
getEntry,
|
|
50777
|
+
listAgentNodeIds: async () => {
|
|
50778
|
+
return (await api.nodes.directory.query()).nodes.filter((n) => n.isOnline && !n.isHub).map((n) => n.id);
|
|
50779
|
+
},
|
|
50780
|
+
distribute: async (mId, nodeId, format) => {
|
|
50781
|
+
const res = await this.distributeModel({
|
|
50782
|
+
modelId: mId,
|
|
50783
|
+
nodeId,
|
|
50784
|
+
format
|
|
50785
|
+
});
|
|
50786
|
+
return {
|
|
50787
|
+
sha256: res.sha256,
|
|
50788
|
+
bytes: res.bytes
|
|
50789
|
+
};
|
|
50790
|
+
}
|
|
50791
|
+
});
|
|
50792
|
+
}
|
|
50793
|
+
/**
|
|
50794
|
+
* Delete one CamStack seed format on one node. Physically removes files from
|
|
50795
|
+
* that node's modelsDir via `pipelineExecutor.deleteModel` + `nodePin`.
|
|
50796
|
+
*/
|
|
50797
|
+
async deleteSeedFromNode(input) {
|
|
50798
|
+
const api = this.ctx.api;
|
|
50799
|
+
if (!api) throw new Error("deleteSeedFromNode: API not available");
|
|
50800
|
+
await api.pipelineExecutor.deleteModel.mutate({
|
|
50801
|
+
addonId: input.addonId,
|
|
50802
|
+
modelId: input.modelId,
|
|
50803
|
+
format: input.format
|
|
50804
|
+
}, nodePin(input.nodeId));
|
|
50805
|
+
this.ctx.logger.info("CamStack seed deleted from node disk", { meta: {
|
|
50806
|
+
modelId: input.modelId,
|
|
50807
|
+
format: input.format,
|
|
50808
|
+
nodeId: input.nodeId,
|
|
50809
|
+
addonId: input.addonId
|
|
50810
|
+
} });
|
|
50811
|
+
return { success: true };
|
|
50812
|
+
}
|
|
50813
|
+
/**
|
|
50814
|
+
* Delete one format on one node. Registry row stays; only that availability
|
|
50815
|
+
* chip is cleared. Pins are left alone — re-Send to node restores files.
|
|
50816
|
+
*/
|
|
50817
|
+
async removeModelFromNode(input) {
|
|
50818
|
+
const descriptor = (await this.state_.get()).find((m) => m.entry.id === input.modelId);
|
|
50819
|
+
if (!descriptor) throw new Error(`removeModelFromNode: model "${input.modelId}" is not registered`);
|
|
50820
|
+
if (descriptor.entry.formats[input.format] === void 0) throw new Error(`removeModelFromNode: model "${input.modelId}" has no ${input.format} format in the Library`);
|
|
50821
|
+
let deleted = false;
|
|
50822
|
+
const api = this.ctx.api;
|
|
50823
|
+
try {
|
|
50824
|
+
if (api) {
|
|
50825
|
+
await api.pipelineExecutor.deleteModel.mutate({
|
|
50826
|
+
addonId: descriptor.stepId,
|
|
50827
|
+
modelId: input.modelId,
|
|
50828
|
+
format: input.format
|
|
50829
|
+
}, nodePin(input.nodeId));
|
|
50830
|
+
deleted = true;
|
|
50831
|
+
}
|
|
50832
|
+
} catch (err) {
|
|
50833
|
+
this.ctx.logger.warn("removeModelFromNode: remote delete failed (continuing)", { meta: {
|
|
50834
|
+
modelId: input.modelId,
|
|
50835
|
+
nodeId: input.nodeId,
|
|
50836
|
+
format: input.format,
|
|
50837
|
+
error: err instanceof Error ? err.message : String(err)
|
|
50838
|
+
} });
|
|
50839
|
+
}
|
|
50840
|
+
if (input.nodeId === "hub") try {
|
|
50841
|
+
(0, _camstack_system_addon_utils.deleteModelFromDisk)(await this.resolveModelsDir(), descriptor.entry, input.format);
|
|
50842
|
+
deleted = true;
|
|
50843
|
+
} catch (err) {
|
|
50844
|
+
this.ctx.logger.warn("removeModelFromNode: hub disk wipe failed", { meta: {
|
|
50845
|
+
modelId: input.modelId,
|
|
50846
|
+
format: input.format,
|
|
50847
|
+
error: err instanceof Error ? err.message : String(err)
|
|
50848
|
+
} });
|
|
50849
|
+
}
|
|
50850
|
+
let clearedAvailability = false;
|
|
50851
|
+
await this.availability_.update((prev) => {
|
|
50852
|
+
const result = clearAvailabilityFormat(prev, input.modelId, input.nodeId, input.format);
|
|
50853
|
+
clearedAvailability = result.cleared;
|
|
50854
|
+
return result.next;
|
|
50855
|
+
});
|
|
50856
|
+
this.ctx.logger.info("Removed custom model format from node", { meta: {
|
|
50857
|
+
modelId: input.modelId,
|
|
50858
|
+
nodeId: input.nodeId,
|
|
50859
|
+
format: input.format,
|
|
50860
|
+
deleted,
|
|
50861
|
+
clearedAvailability
|
|
50862
|
+
} });
|
|
50863
|
+
return {
|
|
50864
|
+
deleted,
|
|
50865
|
+
clearedAvailability
|
|
50866
|
+
};
|
|
50501
50867
|
}
|
|
50868
|
+
/**
|
|
50869
|
+
* Full purge of a Library model: delete artifacts on every node that had
|
|
50870
|
+
* them, clear availability (so Sources no longer shows "imported"), strip
|
|
50871
|
+
* pipeline pins that named this modelId (agents revert to hardware defaults
|
|
50872
|
+
* for that step), then drop the registry row.
|
|
50873
|
+
*/
|
|
50502
50874
|
async removeCustomModel(input) {
|
|
50875
|
+
const descriptor = (await this.state_.get()).find((m) => m.entry.id === input.modelId);
|
|
50876
|
+
if (!descriptor) {
|
|
50877
|
+
await this.availability_.update((prev) => clearAvailability(prev, input.modelId).next);
|
|
50878
|
+
return {
|
|
50879
|
+
removed: false,
|
|
50880
|
+
deletedNodes: [],
|
|
50881
|
+
clearedPins: 0
|
|
50882
|
+
};
|
|
50883
|
+
}
|
|
50884
|
+
const formats = Object.keys(descriptor.entry.formats).filter((f) => descriptor.entry.formats[f] !== void 0);
|
|
50885
|
+
const avail = await this.availability_.get();
|
|
50886
|
+
const nodeIds = new Set(["hub", ...Object.keys(avail[input.modelId] ?? {})]);
|
|
50887
|
+
const deletedNodes = [];
|
|
50888
|
+
const api = this.ctx.api;
|
|
50889
|
+
for (const nodeId of nodeIds) {
|
|
50890
|
+
let anyDeleted = false;
|
|
50891
|
+
for (const format of formats) try {
|
|
50892
|
+
if (api) {
|
|
50893
|
+
await api.pipelineExecutor.deleteModel.mutate({
|
|
50894
|
+
addonId: descriptor.stepId,
|
|
50895
|
+
modelId: input.modelId,
|
|
50896
|
+
format
|
|
50897
|
+
}, nodePin(nodeId));
|
|
50898
|
+
anyDeleted = true;
|
|
50899
|
+
}
|
|
50900
|
+
} catch (err) {
|
|
50901
|
+
this.ctx.logger.warn("removeCustomModel: remote delete failed (continuing)", { meta: {
|
|
50902
|
+
modelId: input.modelId,
|
|
50903
|
+
nodeId,
|
|
50904
|
+
format,
|
|
50905
|
+
error: err instanceof Error ? err.message : String(err)
|
|
50906
|
+
} });
|
|
50907
|
+
}
|
|
50908
|
+
if (anyDeleted) deletedNodes.push(nodeId);
|
|
50909
|
+
}
|
|
50910
|
+
try {
|
|
50911
|
+
const modelsDir = await this.resolveModelsDir();
|
|
50912
|
+
for (const format of formats) (0, _camstack_system_addon_utils.deleteModelFromDisk)(modelsDir, descriptor.entry, format);
|
|
50913
|
+
} catch (err) {
|
|
50914
|
+
this.ctx.logger.warn("removeCustomModel: hub disk wipe failed", { meta: {
|
|
50915
|
+
modelId: input.modelId,
|
|
50916
|
+
error: err instanceof Error ? err.message : String(err)
|
|
50917
|
+
} });
|
|
50918
|
+
}
|
|
50919
|
+
let clearedPins = 0;
|
|
50920
|
+
if (api) {
|
|
50921
|
+
try {
|
|
50922
|
+
clearedPins += await this.clearAgentModelPins(input.modelId);
|
|
50923
|
+
} catch (err) {
|
|
50924
|
+
this.ctx.logger.warn("removeCustomModel: agent pin clear failed", { meta: {
|
|
50925
|
+
modelId: input.modelId,
|
|
50926
|
+
error: err instanceof Error ? err.message : String(err)
|
|
50927
|
+
} });
|
|
50928
|
+
}
|
|
50929
|
+
try {
|
|
50930
|
+
clearedPins += await this.clearCameraModelPins(input.modelId, descriptor.stepId);
|
|
50931
|
+
} catch (err) {
|
|
50932
|
+
this.ctx.logger.warn("removeCustomModel: camera pin clear failed", { meta: {
|
|
50933
|
+
modelId: input.modelId,
|
|
50934
|
+
error: err instanceof Error ? err.message : String(err)
|
|
50935
|
+
} });
|
|
50936
|
+
}
|
|
50937
|
+
}
|
|
50938
|
+
await this.availability_.update((prev) => clearAvailability(prev, input.modelId).next);
|
|
50503
50939
|
let removed = false;
|
|
50504
50940
|
await this.state_.update((prev) => {
|
|
50505
50941
|
const result = removeCustomModel(prev, input.modelId);
|
|
@@ -50508,13 +50944,80 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
|
50508
50944
|
});
|
|
50509
50945
|
this.ctx.logger.info("Removed custom model", { meta: {
|
|
50510
50946
|
modelId: input.modelId,
|
|
50511
|
-
removed
|
|
50947
|
+
removed,
|
|
50948
|
+
deletedNodes,
|
|
50949
|
+
clearedPins
|
|
50512
50950
|
} });
|
|
50513
|
-
return {
|
|
50951
|
+
return {
|
|
50952
|
+
removed,
|
|
50953
|
+
deletedNodes,
|
|
50954
|
+
clearedPins
|
|
50955
|
+
};
|
|
50956
|
+
}
|
|
50957
|
+
/** Strip `modelId` from every agent's inferenceDevices step pins. */
|
|
50958
|
+
async clearAgentModelPins(modelId) {
|
|
50959
|
+
const api = this.ctx.api;
|
|
50960
|
+
if (!api) return 0;
|
|
50961
|
+
const agents = await api.pipelineOrchestrator.listAgentSettings.query();
|
|
50962
|
+
let cleared = 0;
|
|
50963
|
+
for (const agent of agents) {
|
|
50964
|
+
const devices = agent.settings.inferenceDevices;
|
|
50965
|
+
if (devices === void 0) continue;
|
|
50966
|
+
const { next, cleared: n } = stripModelIdFromInferenceDevices(devices, modelId);
|
|
50967
|
+
if (n === 0) continue;
|
|
50968
|
+
await api.pipelineOrchestrator.setAgentInferenceDevices.mutate({
|
|
50969
|
+
agentNodeId: agent.nodeId,
|
|
50970
|
+
inferenceDevices: next
|
|
50971
|
+
});
|
|
50972
|
+
cleared += n;
|
|
50973
|
+
}
|
|
50974
|
+
return cleared;
|
|
50975
|
+
}
|
|
50976
|
+
/**
|
|
50977
|
+
* Clear per-camera step overrides that pin `modelId` for `stepId`.
|
|
50978
|
+
* `patch: null` drops the override so the camera inherits the agent default.
|
|
50979
|
+
*/
|
|
50980
|
+
async clearCameraModelPins(modelId, stepId) {
|
|
50981
|
+
const api = this.ctx.api;
|
|
50982
|
+
if (!api) return 0;
|
|
50983
|
+
const devices = await api.deviceManager.listAll.query({ projection: "slim" });
|
|
50984
|
+
let cleared = 0;
|
|
50985
|
+
for (const device of devices) {
|
|
50986
|
+
const overrides = await api.pipelineOrchestrator.getCameraStepOverrides.query({ deviceId: device.id });
|
|
50987
|
+
if (overrides === null) continue;
|
|
50988
|
+
for (const [agentNodeId, byDeviceKey] of Object.entries(overrides)) for (const [deviceKey, byStep] of Object.entries(byDeviceKey)) {
|
|
50989
|
+
if (byStep[stepId]?.modelId !== modelId) continue;
|
|
50990
|
+
await api.pipelineOrchestrator.setCameraStepOverride.mutate({
|
|
50991
|
+
deviceId: device.id,
|
|
50992
|
+
agentNodeId,
|
|
50993
|
+
deviceKey,
|
|
50994
|
+
addonId: stepId,
|
|
50995
|
+
patch: null
|
|
50996
|
+
});
|
|
50997
|
+
cleared += 1;
|
|
50998
|
+
}
|
|
50999
|
+
}
|
|
51000
|
+
return cleared;
|
|
50514
51001
|
}
|
|
50515
51002
|
async listCustomModels() {
|
|
50516
51003
|
return this.state_.get();
|
|
50517
51004
|
}
|
|
51005
|
+
/**
|
|
51006
|
+
* Self-heal: drop availability rows for modelIds no longer in the Library
|
|
51007
|
+
* registry. Import and Library both trigger this so an orphan cannot keep
|
|
51008
|
+
* Sources stuck on "imported" after a registry wipe.
|
|
51009
|
+
*/
|
|
51010
|
+
async pruneOrphanAvailability_() {
|
|
51011
|
+
try {
|
|
51012
|
+
const [avail, models] = await Promise.all([this.availability_.get(), this.state_.get()]);
|
|
51013
|
+
const { next, pruned } = pruneOrphanAvailability(avail, new Set(models.map((m) => m.entry.id)));
|
|
51014
|
+
if (pruned.length === 0) return;
|
|
51015
|
+
await this.availability_.update(() => next);
|
|
51016
|
+
this.ctx.logger.info("Pruned orphan model availability", { meta: { pruned } });
|
|
51017
|
+
} catch (err) {
|
|
51018
|
+
this.ctx.logger.warn("pruneOrphanAvailability failed (continuing)", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
51019
|
+
}
|
|
51020
|
+
}
|
|
50518
51021
|
};
|
|
50519
51022
|
//#endregion
|
|
50520
51023
|
exports.ModelStudioAddon = ModelStudioAddon;
|