@camstack/addon-model-studio 1.1.198 → 1.1.200
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 +6 -30
- package/dist/{MotionZonesSettings-JRWr-847.mjs → MotionZonesSettings-DXWpYKPD.mjs} +2 -2
- package/dist/{PrivacyMaskSettings-BfpUuDZo.mjs → PrivacyMaskSettings-BcaCmwR2.mjs} +4 -4
- package/dist/{SceneMonitorEditor-B6q_snqj.mjs → SceneMonitorEditor-D8IoPtTi.mjs} +3 -3
- package/dist/_stub.js +9033 -6124
- package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-BmFXvTL5.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-ChSw1hrV.mjs} +3 -3
- package/dist/_virtual_mf___mfe_internal__addon_model_studio_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-C0rOztYh.mjs +26 -0
- package/dist/addon-model-studio.css +1 -1
- package/dist/{hostInit-DWCuJ3yO.mjs → hostInit-TRVWPuo-.mjs} +2 -2
- package/dist/model-studio.addon.js +870 -431
- package/dist/model-studio.addon.mjs +870 -436
- package/dist/{player-overlays-RAKUtQ68.mjs → player-overlays-4Vb0Si7c.mjs} +2 -2
- package/dist/remoteEntry.js +1 -1
- package/dist/{responsive-Cr8pn1rO.mjs → responsive-D8JojVmZ.mjs} +1 -1
- package/dist/{scene-monitor-copy-NIVM76QJ.mjs → scene-monitor-copy-Cw3r5D6b.mjs} +1 -1
- package/dist/{square-BOlfCWYp.mjs → square-C4Y02RV4.mjs} +1 -1
- package/dist/{trash-2-aAfBxjgh.mjs → trash-2-D-gDRhvd.mjs} +1 -1
- package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-BhhyZUuH.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-Bs1AQFNt.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`. */
|
|
@@ -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
|
|
@@ -21683,39 +21507,19 @@ Object.freeze(Object.fromEntries([{
|
|
|
21683
21507
|
label: "Face recognition model",
|
|
21684
21508
|
defaultModelId: "auraface-r100",
|
|
21685
21509
|
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
|
-
]
|
|
21510
|
+
options: [{
|
|
21511
|
+
id: "auraface-r100",
|
|
21512
|
+
label: "AuraFace R100"
|
|
21513
|
+
}]
|
|
21700
21514
|
}, {
|
|
21701
21515
|
stepId: "clip-embedding",
|
|
21702
21516
|
label: "Semantic search model",
|
|
21703
|
-
defaultModelId: "
|
|
21517
|
+
defaultModelId: "siglip2-b16-224",
|
|
21704
21518
|
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
|
-
]
|
|
21519
|
+
options: [{
|
|
21520
|
+
id: "siglip2-b16-224",
|
|
21521
|
+
label: "SigLIP2 B/16 (Apache-2.0)"
|
|
21522
|
+
}]
|
|
21719
21523
|
}].map((s) => [s.stepId, s.defaultModelId])));
|
|
21720
21524
|
var CLUSTER_STEP_SETTING_FIELDS = [{
|
|
21721
21525
|
stepId: "face-embedding",
|
|
@@ -32427,7 +32231,13 @@ var OsdStatusSchema = object({ overlays: array(object({
|
|
|
32427
32231
|
*/
|
|
32428
32232
|
availablePositions: array(OsdPositionEnum).readonly(),
|
|
32429
32233
|
/** Firmware does not allow editing the `text` field (e.g. timestamp). */
|
|
32430
|
-
readOnly: boolean().optional()
|
|
32234
|
+
readOnly: boolean().optional(),
|
|
32235
|
+
/**
|
|
32236
|
+
* Firmware text ceiling for this overlay. Absent = no known limit.
|
|
32237
|
+
* The OSD manager truncates here on every write — never stores its own
|
|
32238
|
+
* copy on the binding.
|
|
32239
|
+
*/
|
|
32240
|
+
maxCharacters: number().int().positive().optional()
|
|
32431
32241
|
})).readonly() });
|
|
32432
32242
|
var OsdOverlayPatchSchema = object({
|
|
32433
32243
|
enabled: boolean().optional(),
|
|
@@ -32478,7 +32288,9 @@ var OsdTemplateBindingSchema = object({
|
|
|
32478
32288
|
valuePath: string().min(1).max(64),
|
|
32479
32289
|
/** Convert TO this unit at render (`tryConvertUnit`). Absent = source unit. */
|
|
32480
32290
|
displayUnit: string().max(16).optional(),
|
|
32481
|
-
maxDecimals: number().int().min(0).max(4).default(1)
|
|
32291
|
+
maxDecimals: number().int().min(0).max(4).default(1),
|
|
32292
|
+
/** Raw value → display text (same shape as slot binding). Empty string hides. */
|
|
32293
|
+
valueMap: record(string(), string()).optional()
|
|
32482
32294
|
});
|
|
32483
32295
|
/**
|
|
32484
32296
|
* A named template: bindings (alias → leaf field) + a parser that
|
|
@@ -32528,18 +32340,11 @@ var OsdSlotBindingSchema = object({
|
|
|
32528
32340
|
source: OsdSourceSchema,
|
|
32529
32341
|
/** `${value}` and `${unit}` are substituted; every occurrence. */
|
|
32530
32342
|
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
32343
|
/**
|
|
32534
32344
|
* Decimal places for a numeric value. `0` yields an integer — the
|
|
32535
32345
|
* documented workaround for firmwares that reject `.` in overlay text.
|
|
32536
32346
|
*/
|
|
32537
32347
|
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
32348
|
/** Raw value → display text, e.g. `{"true":"MOTION","false":""}`. */
|
|
32544
32349
|
valueMap: record(string(), string()).optional(),
|
|
32545
32350
|
/** Time windows in which the slot is shown. Absent = always. */
|
|
@@ -32548,9 +32353,7 @@ var OsdSlotBindingSchema = object({
|
|
|
32548
32353
|
* Display gate, in the notification centre's condition vocabulary.
|
|
32549
32354
|
* Only the keys reported by `getConditionSupport` are accepted.
|
|
32550
32355
|
*/
|
|
32551
|
-
conditions: NcConditionsSchema.optional()
|
|
32552
|
-
/** Rendered when the gate is closed or the value unreadable. Empty = hide. */
|
|
32553
|
-
fallbackText: string().max(64).default("")
|
|
32356
|
+
conditions: NcConditionsSchema.optional()
|
|
32554
32357
|
});
|
|
32555
32358
|
/** One camera slot, as the operator sees it: firmware truth + our binding. */
|
|
32556
32359
|
var OsdSlotViewSchema = object({
|
|
@@ -48522,6 +48325,8 @@ var STATE_SUMMARY_VARS = [
|
|
|
48522
48325
|
[...RULE_TEXT],
|
|
48523
48326
|
[...RULE_TEXT],
|
|
48524
48327
|
[...RULE_TEXT],
|
|
48328
|
+
[...RULE_TEXT],
|
|
48329
|
+
[...RULE_TEXT],
|
|
48525
48330
|
...STATE_SUMMARY_VARS
|
|
48526
48331
|
];
|
|
48527
48332
|
new Set([
|
|
@@ -49102,7 +48907,9 @@ async function convertModel(input, deps) {
|
|
|
49102
48907
|
var ConvertProgressSchema = object({
|
|
49103
48908
|
phase: string(),
|
|
49104
48909
|
pct: number().optional(),
|
|
49105
|
-
detail: string().optional()
|
|
48910
|
+
detail: string().optional(),
|
|
48911
|
+
/** Raw process line (stderr) — UI log pane, not a phase step. */
|
|
48912
|
+
log: string().optional()
|
|
49106
48913
|
});
|
|
49107
48914
|
var ConvertResultLineSchema = object({ result: object({ artifacts: array(ConvertArtifactSchema) }) });
|
|
49108
48915
|
function buildEntry(metadata, artifacts) {
|
|
@@ -49171,29 +48978,57 @@ function buildModelConvertProvider(deps) {
|
|
|
49171
48978
|
//#endregion
|
|
49172
48979
|
//#region src/model-registry.ts
|
|
49173
48980
|
/**
|
|
49174
|
-
*
|
|
49175
|
-
*
|
|
48981
|
+
* Record that `modelId` now has `format` on `nodeId`. Merges into the node's
|
|
48982
|
+
* formats map — never drops a sibling format on the same node.
|
|
49176
48983
|
*/
|
|
49177
|
-
function
|
|
48984
|
+
function upsertAvailabilityFormat(map, modelId, nodeId, format, presence) {
|
|
48985
|
+
const prevNode = map[modelId]?.[nodeId] ?? { formats: {} };
|
|
49178
48986
|
return {
|
|
49179
48987
|
...map,
|
|
49180
48988
|
[modelId]: {
|
|
49181
48989
|
...map[modelId],
|
|
49182
|
-
[nodeId]:
|
|
48990
|
+
[nodeId]: { formats: {
|
|
48991
|
+
...prevNode.formats,
|
|
48992
|
+
[format]: presence
|
|
48993
|
+
} }
|
|
49183
48994
|
}
|
|
49184
48995
|
};
|
|
49185
48996
|
}
|
|
49186
48997
|
/**
|
|
48998
|
+
* @deprecated Prefer {@link upsertAvailabilityFormat}. Kept for call sites that
|
|
48999
|
+
* still pass a single-format legacy shape — folds into multi-format.
|
|
49000
|
+
*/
|
|
49001
|
+
function upsertAvailability(map, modelId, nodeId, entry) {
|
|
49002
|
+
return upsertAvailabilityFormat(map, modelId, nodeId, entry.format, {
|
|
49003
|
+
sha256: entry.sha256,
|
|
49004
|
+
bytes: entry.bytes,
|
|
49005
|
+
at: entry.at
|
|
49006
|
+
});
|
|
49007
|
+
}
|
|
49008
|
+
/**
|
|
49187
49009
|
* Pure registry operations over the custom-model list. Kept side-effect-free
|
|
49188
49010
|
* so they unit-test without a live `BaseAddon` context — the addon class is a
|
|
49189
49011
|
* thin durable-state wrapper around these.
|
|
49190
49012
|
*/
|
|
49191
49013
|
/**
|
|
49192
|
-
* Insert or replace a descriptor keyed by `entry.id
|
|
49193
|
-
*
|
|
49014
|
+
* Insert or replace a descriptor keyed by `entry.id`. When the id already
|
|
49015
|
+
* exists, **formats are merged** (Scrypted multi-format accumulate) — sibling
|
|
49016
|
+
* formats on the prior entry are never dropped. Returns a new array.
|
|
49194
49017
|
*/
|
|
49195
49018
|
function upsertCustomModel(list, descriptor) {
|
|
49196
|
-
|
|
49019
|
+
const prev = list.find((m) => m.entry.id === descriptor.entry.id);
|
|
49020
|
+
const merged = prev === void 0 ? descriptor : {
|
|
49021
|
+
stepId: descriptor.stepId,
|
|
49022
|
+
entry: {
|
|
49023
|
+
...prev.entry,
|
|
49024
|
+
...descriptor.entry,
|
|
49025
|
+
formats: {
|
|
49026
|
+
...prev.entry.formats,
|
|
49027
|
+
...descriptor.entry.formats
|
|
49028
|
+
}
|
|
49029
|
+
}
|
|
49030
|
+
};
|
|
49031
|
+
return [...list.filter((m) => m.entry.id !== descriptor.entry.id), merged];
|
|
49197
49032
|
}
|
|
49198
49033
|
/** Drop the descriptor with the given `entry.id`. Reports whether anything changed. */
|
|
49199
49034
|
function removeCustomModel(list, modelId) {
|
|
@@ -49203,6 +49038,114 @@ function removeCustomModel(list, modelId) {
|
|
|
49203
49038
|
removed: next.length !== list.length
|
|
49204
49039
|
};
|
|
49205
49040
|
}
|
|
49041
|
+
/** Drop every node entry for `modelId` from the availability map. */
|
|
49042
|
+
function clearAvailability(map, modelId) {
|
|
49043
|
+
if (map[modelId] === void 0) return {
|
|
49044
|
+
next: map,
|
|
49045
|
+
hadEntry: false
|
|
49046
|
+
};
|
|
49047
|
+
const next = { ...map };
|
|
49048
|
+
delete next[modelId];
|
|
49049
|
+
return {
|
|
49050
|
+
next,
|
|
49051
|
+
hadEntry: true
|
|
49052
|
+
};
|
|
49053
|
+
}
|
|
49054
|
+
/**
|
|
49055
|
+
* Drop one `(modelId, nodeId, format)` from availability. Empty nodes and empty
|
|
49056
|
+
* model rows are pruned so Library "On nodes" does not keep a ghost chip.
|
|
49057
|
+
*/
|
|
49058
|
+
function clearAvailabilityFormat(map, modelId, nodeId, format) {
|
|
49059
|
+
const perNode = map[modelId];
|
|
49060
|
+
if (perNode === void 0) return {
|
|
49061
|
+
next: map,
|
|
49062
|
+
cleared: false
|
|
49063
|
+
};
|
|
49064
|
+
const node = perNode[nodeId];
|
|
49065
|
+
if (node === void 0 || node.formats[format] === void 0) return {
|
|
49066
|
+
next: map,
|
|
49067
|
+
cleared: false
|
|
49068
|
+
};
|
|
49069
|
+
const formats = { ...node.formats };
|
|
49070
|
+
delete formats[format];
|
|
49071
|
+
const nextNodes = { ...perNode };
|
|
49072
|
+
if (Object.keys(formats).length === 0) delete nextNodes[nodeId];
|
|
49073
|
+
else nextNodes[nodeId] = { formats };
|
|
49074
|
+
if (Object.keys(nextNodes).length === 0) {
|
|
49075
|
+
const next = { ...map };
|
|
49076
|
+
delete next[modelId];
|
|
49077
|
+
return {
|
|
49078
|
+
next,
|
|
49079
|
+
cleared: true
|
|
49080
|
+
};
|
|
49081
|
+
}
|
|
49082
|
+
return {
|
|
49083
|
+
next: {
|
|
49084
|
+
...map,
|
|
49085
|
+
[modelId]: nextNodes
|
|
49086
|
+
},
|
|
49087
|
+
cleared: true
|
|
49088
|
+
};
|
|
49089
|
+
}
|
|
49090
|
+
/**
|
|
49091
|
+
* Drop availability rows whose modelId is not in the Library registry.
|
|
49092
|
+
* Orphans left Sources showing "imported" after a registry-only wipe.
|
|
49093
|
+
*/
|
|
49094
|
+
function pruneOrphanAvailability(map, registeredIds) {
|
|
49095
|
+
const pruned = [];
|
|
49096
|
+
let next = null;
|
|
49097
|
+
for (const modelId of Object.keys(map)) {
|
|
49098
|
+
if (registeredIds.has(modelId)) continue;
|
|
49099
|
+
if (next === null) next = { ...map };
|
|
49100
|
+
delete next[modelId];
|
|
49101
|
+
pruned.push(modelId);
|
|
49102
|
+
}
|
|
49103
|
+
return {
|
|
49104
|
+
next: next ?? map,
|
|
49105
|
+
pruned
|
|
49106
|
+
};
|
|
49107
|
+
}
|
|
49108
|
+
function stripModelIdFromInferenceDevices(devices, modelId) {
|
|
49109
|
+
let cleared = 0;
|
|
49110
|
+
const next = {};
|
|
49111
|
+
for (const [key, device] of Object.entries(devices)) {
|
|
49112
|
+
const stepsIn = device.steps;
|
|
49113
|
+
if (stepsIn === void 0) {
|
|
49114
|
+
next[key] = {
|
|
49115
|
+
enabled: device.enabled,
|
|
49116
|
+
...device.weight !== void 0 ? { weight: device.weight } : {},
|
|
49117
|
+
...device.maxSessions !== void 0 ? { maxSessions: device.maxSessions } : {}
|
|
49118
|
+
};
|
|
49119
|
+
continue;
|
|
49120
|
+
}
|
|
49121
|
+
const stepsOut = {};
|
|
49122
|
+
for (const [stepId, step] of Object.entries(stepsIn)) {
|
|
49123
|
+
if (step.modelId === modelId) {
|
|
49124
|
+
cleared += 1;
|
|
49125
|
+
const kept = {};
|
|
49126
|
+
if (step.settings !== void 0) kept.settings = { ...step.settings };
|
|
49127
|
+
if (step.jumpDeviceKey !== void 0) kept.jumpDeviceKey = step.jumpDeviceKey;
|
|
49128
|
+
if (Object.keys(kept).length > 0) stepsOut[stepId] = kept;
|
|
49129
|
+
continue;
|
|
49130
|
+
}
|
|
49131
|
+
stepsOut[stepId] = {
|
|
49132
|
+
...step.modelId !== void 0 ? { modelId: step.modelId } : {},
|
|
49133
|
+
...step.settings !== void 0 ? { settings: { ...step.settings } } : {},
|
|
49134
|
+
...step.jumpDeviceKey !== void 0 ? { jumpDeviceKey: step.jumpDeviceKey } : {}
|
|
49135
|
+
};
|
|
49136
|
+
}
|
|
49137
|
+
next[key] = {
|
|
49138
|
+
enabled: device.enabled,
|
|
49139
|
+
...device.weight !== void 0 ? { weight: device.weight } : {},
|
|
49140
|
+
...device.maxSessions !== void 0 ? { maxSessions: device.maxSessions } : {},
|
|
49141
|
+
...Object.keys(stepsOut).length > 0 ? { steps: stepsOut } : {}
|
|
49142
|
+
};
|
|
49143
|
+
}
|
|
49144
|
+
return {
|
|
49145
|
+
next,
|
|
49146
|
+
cleared
|
|
49147
|
+
};
|
|
49148
|
+
}
|
|
49206
49149
|
//#endregion
|
|
49207
49150
|
//#region src/frigate/frigate-import.ts
|
|
49208
49151
|
/**
|
|
@@ -49438,6 +49381,64 @@ async function runPlusConversions(deps, args) {
|
|
|
49438
49381
|
return outcomes;
|
|
49439
49382
|
}
|
|
49440
49383
|
//#endregion
|
|
49384
|
+
//#region src/ensure-formats.ts
|
|
49385
|
+
var ENGINE_FORMATS = ["openvino", "coreml"];
|
|
49386
|
+
function missingEngineFormats(entry) {
|
|
49387
|
+
return ENGINE_FORMATS.filter((f) => entry.formats[f] === void 0);
|
|
49388
|
+
}
|
|
49389
|
+
async function ensureEngineFormats(entry, deps, opts = {}) {
|
|
49390
|
+
if (entry.formats.onnx === void 0) return {
|
|
49391
|
+
entry,
|
|
49392
|
+
conversions: []
|
|
49393
|
+
};
|
|
49394
|
+
const missing = (opts.targetFormats ?? ENGINE_FORMATS).filter((f) => entry.formats[f] === void 0);
|
|
49395
|
+
if (missing.length === 0) return {
|
|
49396
|
+
entry,
|
|
49397
|
+
conversions: []
|
|
49398
|
+
};
|
|
49399
|
+
return {
|
|
49400
|
+
entry,
|
|
49401
|
+
conversions: await runPlusConversions(deps, {
|
|
49402
|
+
entry,
|
|
49403
|
+
targetFormats: missing
|
|
49404
|
+
})
|
|
49405
|
+
};
|
|
49406
|
+
}
|
|
49407
|
+
/**
|
|
49408
|
+
* Ensure OV/CoreML from ONNX when missing, then push every declared format to
|
|
49409
|
+
* every agent (hub is skipped — it already hosts imports).
|
|
49410
|
+
*/
|
|
49411
|
+
async function ensureFormatsThenDistribute(modelId, deps) {
|
|
49412
|
+
const initial = await deps.getEntry();
|
|
49413
|
+
if (initial === void 0) throw new Error(`ensureFormatsThenDistribute: model "${modelId}" is not registered`);
|
|
49414
|
+
let conversions = [];
|
|
49415
|
+
if (initial.formats.onnx !== void 0 && missingEngineFormats(initial).length > 0) conversions = (await ensureEngineFormats(initial, deps)).conversions;
|
|
49416
|
+
const entry = await deps.getEntry() ?? initial;
|
|
49417
|
+
const formats = Object.keys(entry.formats).filter((f) => entry.formats[f] !== void 0);
|
|
49418
|
+
const agents = await deps.listAgentNodeIds();
|
|
49419
|
+
const distributed = [];
|
|
49420
|
+
for (const nodeId of agents) for (const format of formats) try {
|
|
49421
|
+
const res = await deps.distribute(modelId, nodeId, format);
|
|
49422
|
+
distributed.push({
|
|
49423
|
+
nodeId,
|
|
49424
|
+
format,
|
|
49425
|
+
ok: true,
|
|
49426
|
+
bytes: res.bytes
|
|
49427
|
+
});
|
|
49428
|
+
} catch (err) {
|
|
49429
|
+
distributed.push({
|
|
49430
|
+
nodeId,
|
|
49431
|
+
format,
|
|
49432
|
+
ok: false,
|
|
49433
|
+
error: err instanceof Error ? err.message : String(err)
|
|
49434
|
+
});
|
|
49435
|
+
}
|
|
49436
|
+
return {
|
|
49437
|
+
conversions,
|
|
49438
|
+
distributed
|
|
49439
|
+
};
|
|
49440
|
+
}
|
|
49441
|
+
//#endregion
|
|
49441
49442
|
//#region src/frigate/frigate-plus.ts
|
|
49442
49443
|
var PLUS_API_HOST = "https://api.frigate.video";
|
|
49443
49444
|
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 +49542,12 @@ var PLUS_TYPE_MAP = {
|
|
|
49541
49542
|
postprocessor: "ssd",
|
|
49542
49543
|
extension: ".tflite"
|
|
49543
49544
|
},
|
|
49545
|
+
/** List API uses `ssd` for MobileDet rows. */
|
|
49546
|
+
ssd: {
|
|
49547
|
+
format: "tflite",
|
|
49548
|
+
postprocessor: "ssd",
|
|
49549
|
+
extension: ".tflite"
|
|
49550
|
+
},
|
|
49544
49551
|
yolonas: {
|
|
49545
49552
|
format: "onnx",
|
|
49546
49553
|
postprocessor: "yolonas",
|
|
@@ -49549,6 +49556,10 @@ var PLUS_TYPE_MAP = {
|
|
|
49549
49556
|
yolov9: {
|
|
49550
49557
|
format: "onnx",
|
|
49551
49558
|
extension: ".onnx"
|
|
49559
|
+
},
|
|
49560
|
+
"yolo-generic": {
|
|
49561
|
+
format: "onnx",
|
|
49562
|
+
extension: ".onnx"
|
|
49552
49563
|
}
|
|
49553
49564
|
};
|
|
49554
49565
|
function plusTypeTraits(type) {
|
|
@@ -49573,7 +49584,16 @@ function orderedLabels(labelMap) {
|
|
|
49573
49584
|
* Distribute.
|
|
49574
49585
|
*/
|
|
49575
49586
|
function plusInfoToDescriptor(info, opts) {
|
|
49576
|
-
const
|
|
49587
|
+
const base = plusTypeTraits(info.type);
|
|
49588
|
+
const traits = opts.format === "tflite" ? {
|
|
49589
|
+
format: "tflite",
|
|
49590
|
+
extension: ".tflite",
|
|
49591
|
+
...info.type === "ssd" || info.type === "mobiledet" ? { postprocessor: "ssd" } : {}
|
|
49592
|
+
} : opts.format === "onnx" ? {
|
|
49593
|
+
format: "onnx",
|
|
49594
|
+
extension: ".onnx",
|
|
49595
|
+
...base.postprocessor !== void 0 ? { postprocessor: base.postprocessor } : {}
|
|
49596
|
+
} : base;
|
|
49577
49597
|
const labels = orderedLabels(info.labelMap);
|
|
49578
49598
|
return {
|
|
49579
49599
|
stepId: "object-detection",
|
|
@@ -49691,15 +49711,33 @@ var ModelUploadStore = class {
|
|
|
49691
49711
|
* only the read-array `listModels` (auto-concatenated across providers); the
|
|
49692
49712
|
* mutations live here so the cap stays a clean collection.
|
|
49693
49713
|
*/
|
|
49694
|
-
/** One
|
|
49695
|
-
var
|
|
49696
|
-
format: _enum(MODEL_FORMATS),
|
|
49714
|
+
/** One format's presence on a node. */
|
|
49715
|
+
var ModelFormatPresenceSchema = object({
|
|
49697
49716
|
sha256: string(),
|
|
49698
49717
|
bytes: number(),
|
|
49699
49718
|
at: number()
|
|
49700
49719
|
});
|
|
49701
|
-
/**
|
|
49720
|
+
/**
|
|
49721
|
+
* Per-node availability — multi-format. Legacy durable rows were
|
|
49722
|
+
* `{ format, sha256, bytes, at }`; preprocess folds them into `formats`.
|
|
49723
|
+
*/
|
|
49724
|
+
var ModelNodeAvailabilitySchema = preprocess((raw) => {
|
|
49725
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return raw;
|
|
49726
|
+
const rec = raw;
|
|
49727
|
+
if (typeof rec.formats === "object" && rec.formats !== null && !Array.isArray(rec.formats)) return { formats: rec.formats };
|
|
49728
|
+
if (typeof rec.format === "string") return { formats: { [rec.format]: {
|
|
49729
|
+
sha256: typeof rec.sha256 === "string" ? rec.sha256 : "",
|
|
49730
|
+
bytes: typeof rec.bytes === "number" ? rec.bytes : 0,
|
|
49731
|
+
at: typeof rec.at === "number" ? rec.at : 0
|
|
49732
|
+
} } };
|
|
49733
|
+
return raw;
|
|
49734
|
+
}, object({ formats: partialRecord(_enum(MODEL_FORMATS), ModelFormatPresenceSchema) }));
|
|
49735
|
+
/** `modelId → nodeId → { formats: { [format]: presence } }`. */
|
|
49702
49736
|
var ModelAvailabilityMapSchema = record(string(), record(string(), ModelNodeAvailabilitySchema));
|
|
49737
|
+
/** Stable Library id for a Frigate+ training (chip id ↔ registry). */
|
|
49738
|
+
function frigatePlusRegistryId(plusModelId) {
|
|
49739
|
+
return `frigate-plus-${plusModelId}`;
|
|
49740
|
+
}
|
|
49703
49741
|
var ScryptedManifestFileSchema = object({
|
|
49704
49742
|
relativePath: string(),
|
|
49705
49743
|
size: number(),
|
|
@@ -49772,8 +49810,75 @@ var FrigatePlusModelSchema = object({
|
|
|
49772
49810
|
height: number().optional()
|
|
49773
49811
|
});
|
|
49774
49812
|
var modelStudioActions = defineCustomActions({
|
|
49775
|
-
registerCustomModel: customAction(CustomModelDescriptorSchema, object({
|
|
49776
|
-
|
|
49813
|
+
registerCustomModel: customAction(CustomModelDescriptorSchema, object({
|
|
49814
|
+
ok: literal(true),
|
|
49815
|
+
conversions: array(object({
|
|
49816
|
+
format: _enum(["openvino", "coreml"]),
|
|
49817
|
+
ok: boolean(),
|
|
49818
|
+
nodeId: string().optional(),
|
|
49819
|
+
error: string().optional()
|
|
49820
|
+
})).readonly()
|
|
49821
|
+
}), { kind: "mutation" }),
|
|
49822
|
+
/**
|
|
49823
|
+
* Ensure OV/CoreML from ONNX when missing, then distribute every declared
|
|
49824
|
+
* format to every online agent (hub skipped).
|
|
49825
|
+
*/
|
|
49826
|
+
ensureFormatsThenDistribute: customAction(object({ modelId: string() }), object({
|
|
49827
|
+
conversions: array(object({
|
|
49828
|
+
format: _enum(["openvino", "coreml"]),
|
|
49829
|
+
ok: boolean(),
|
|
49830
|
+
nodeId: string().optional(),
|
|
49831
|
+
error: string().optional()
|
|
49832
|
+
})).readonly(),
|
|
49833
|
+
distributed: array(object({
|
|
49834
|
+
nodeId: string(),
|
|
49835
|
+
format: _enum(MODEL_FORMATS),
|
|
49836
|
+
ok: boolean(),
|
|
49837
|
+
error: string().optional(),
|
|
49838
|
+
bytes: number().optional()
|
|
49839
|
+
})).readonly()
|
|
49840
|
+
}), { kind: "mutation" }),
|
|
49841
|
+
/**
|
|
49842
|
+
* Convert missing OpenVINO / CoreML from an already-registered ONNX entry
|
|
49843
|
+
* (Frigate+ / Custom per-format Import). Only the requested formats run.
|
|
49844
|
+
*/
|
|
49845
|
+
ensureModelFormats: customAction(object({
|
|
49846
|
+
modelId: string(),
|
|
49847
|
+
formats: array(_enum(["openvino", "coreml"])).min(1).readonly()
|
|
49848
|
+
}), object({ conversions: array(object({
|
|
49849
|
+
format: _enum(["openvino", "coreml"]),
|
|
49850
|
+
ok: boolean(),
|
|
49851
|
+
nodeId: string().optional(),
|
|
49852
|
+
error: string().optional()
|
|
49853
|
+
})).readonly() }), { kind: "mutation" }),
|
|
49854
|
+
removeCustomModel: customAction(object({ modelId: string() }), object({
|
|
49855
|
+
removed: boolean(),
|
|
49856
|
+
deletedNodes: array(string()).readonly(),
|
|
49857
|
+
clearedPins: number().int().nonnegative()
|
|
49858
|
+
}), { kind: "mutation" }),
|
|
49859
|
+
/**
|
|
49860
|
+
* Delete one format of a Library model from one node. Keeps the registry row
|
|
49861
|
+
* and sibling formats/nodes; clears only that availability chip.
|
|
49862
|
+
*/
|
|
49863
|
+
removeModelFromNode: customAction(object({
|
|
49864
|
+
modelId: string(),
|
|
49865
|
+
nodeId: string(),
|
|
49866
|
+
format: _enum(MODEL_FORMATS)
|
|
49867
|
+
}), object({
|
|
49868
|
+
deleted: boolean(),
|
|
49869
|
+
clearedAvailability: boolean()
|
|
49870
|
+
}), { kind: "mutation" }),
|
|
49871
|
+
/**
|
|
49872
|
+
* Wipe one CamStack seed format from a node's modelsDir (Installed Remove).
|
|
49873
|
+
* Routes via `nodePin` — `pipelineExecutor.deleteModel` has no `nodeId` in
|
|
49874
|
+
* its input schema, so a bare tRPC mutate always hit the hub.
|
|
49875
|
+
*/
|
|
49876
|
+
deleteSeedFromNode: customAction(object({
|
|
49877
|
+
addonId: string(),
|
|
49878
|
+
modelId: string(),
|
|
49879
|
+
format: _enum(MODEL_FORMATS),
|
|
49880
|
+
nodeId: string()
|
|
49881
|
+
}), object({ success: literal(true) }), { kind: "mutation" }),
|
|
49777
49882
|
listCustomModels: customAction(_void(), array(CustomModelDescriptorSchema).readonly()),
|
|
49778
49883
|
/**
|
|
49779
49884
|
* Distribute a registered model's FORMAT (resident on the hub) to a target
|
|
@@ -49813,17 +49918,16 @@ var modelStudioActions = defineCustomActions({
|
|
|
49813
49918
|
}), { kind: "mutation" }),
|
|
49814
49919
|
abortModelUpload: customAction(object({ uploadId: string() }), object({ ok: literal(true) }), { kind: "mutation" }),
|
|
49815
49920
|
/**
|
|
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).
|
|
49921
|
+
* Curated PUBLIC Frigate model catalog (D214 addendum). Import registers the
|
|
49922
|
+
* descriptor with its public URL (Library only). Optional `targetNodeId`
|
|
49923
|
+
* downloads on that node — no hub staging disk.
|
|
49821
49924
|
*/
|
|
49822
49925
|
listFrigateCatalog: customAction(_void(), array(FrigateCatalogEntrySchema).readonly()),
|
|
49823
49926
|
importFrigateCatalogModel: customAction(object({
|
|
49824
49927
|
catalogId: string(),
|
|
49825
49928
|
modelId: string().optional(),
|
|
49826
|
-
name: string().optional()
|
|
49929
|
+
name: string().optional(),
|
|
49930
|
+
targetNodeId: string().optional()
|
|
49827
49931
|
}), object({
|
|
49828
49932
|
ok: literal(true),
|
|
49829
49933
|
modelId: string(),
|
|
@@ -49832,13 +49936,17 @@ var modelStudioActions = defineCustomActions({
|
|
|
49832
49936
|
/**
|
|
49833
49937
|
* Local Scrypted YOLO catalog (Task 7B). Listing is a projection of the
|
|
49834
49938
|
* revision-pinned manifesto plus a read-only HF drift check. Import accepts
|
|
49835
|
-
*
|
|
49939
|
+
* `manifestId` (+ optional land-on-node) — host/repo/revision/path/labels/
|
|
49940
|
+
* checksum stay server-owned.
|
|
49836
49941
|
*/
|
|
49837
49942
|
listScryptedCatalog: customAction(_void(), object({
|
|
49838
49943
|
entries: array(ScryptedManifestEntrySchema).readonly(),
|
|
49839
49944
|
drift: array(ScryptedCatalogDriftSchema).readonly()
|
|
49840
49945
|
})),
|
|
49841
|
-
importScryptedCatalogModel: customAction(object({
|
|
49946
|
+
importScryptedCatalogModel: customAction(object({
|
|
49947
|
+
manifestId: string(),
|
|
49948
|
+
targetNodeId: string().optional()
|
|
49949
|
+
}), CustomModelDescriptorSchema, { kind: "mutation" }),
|
|
49842
49950
|
/**
|
|
49843
49951
|
* Frigate+ personal models. The API key lives in the addon's SETTINGS
|
|
49844
49952
|
* (password field) and never leaves the hub: list / test / import all run
|
|
@@ -49848,9 +49956,9 @@ var modelStudioActions = defineCustomActions({
|
|
|
49848
49956
|
/**
|
|
49849
49957
|
* Nodes whose engine can actually LOAD a tflite artifact (the pool's only
|
|
49850
49958
|
* tflite runtime binds the Coral EdgeTPU delegate — no Coral, no tflite).
|
|
49851
|
-
*
|
|
49959
|
+
* Empty object — `addons.custom` cannot carry zod `void`.
|
|
49852
49960
|
*/
|
|
49853
|
-
tfliteSupport: customAction(
|
|
49961
|
+
tfliteSupport: customAction(object({}).optional(), object({ nodes: array(string()).readonly() })),
|
|
49854
49962
|
frigatePlusStatus: customAction(_void(), object({
|
|
49855
49963
|
configured: boolean(),
|
|
49856
49964
|
keyFormatValid: boolean()
|
|
@@ -49862,8 +49970,21 @@ var modelStudioActions = defineCustomActions({
|
|
|
49862
49970
|
frigatePlusListModels: customAction(_void(), array(FrigatePlusModelSchema).readonly()),
|
|
49863
49971
|
importFrigatePlusModel: customAction(object({
|
|
49864
49972
|
plusModelId: string(),
|
|
49865
|
-
|
|
49866
|
-
|
|
49973
|
+
/** Ignored for identity — registry id is always `frigate-plus-<plusModelId>`. */
|
|
49974
|
+
modelId: string().optional(),
|
|
49975
|
+
name: string().optional(),
|
|
49976
|
+
/**
|
|
49977
|
+
* Native artifact class from the list row (`plusDownloadTraits`). The
|
|
49978
|
+
* model-detail API does not always carry `supportedDetectors`, so the
|
|
49979
|
+
* browser passes what the catalog already knew (fixes ssd/yolo-generic
|
|
49980
|
+
* EdgeTPU rows that `plusTypeTraits` alone would call onnx).
|
|
49981
|
+
*/
|
|
49982
|
+
format: _enum(["onnx", "tflite"]).optional(),
|
|
49983
|
+
/**
|
|
49984
|
+
* Engine formats to convert after the native artifact lands. Empty =
|
|
49985
|
+
* download + register only (per-format Import clicks OV/CoreML later).
|
|
49986
|
+
*/
|
|
49987
|
+
ensureFormats: array(_enum(["openvino", "coreml"])).readonly().optional()
|
|
49867
49988
|
}), object({
|
|
49868
49989
|
ok: literal(true),
|
|
49869
49990
|
modelId: string(),
|
|
@@ -49946,6 +50067,15 @@ function spawnRunConvert(jobPath, pythonPath, onProgress, _sessionId) {
|
|
|
49946
50067
|
});
|
|
49947
50068
|
proc.stderr.on("data", (chunk) => {
|
|
49948
50069
|
stderrChunks.push(chunk);
|
|
50070
|
+
for (const line of chunk.toString().split("\n")) {
|
|
50071
|
+
const trimmed = line.trimEnd();
|
|
50072
|
+
if (trimmed.length === 0) continue;
|
|
50073
|
+
onProgress({
|
|
50074
|
+
phase: "log",
|
|
50075
|
+
log: trimmed,
|
|
50076
|
+
detail: trimmed
|
|
50077
|
+
});
|
|
50078
|
+
}
|
|
49949
50079
|
});
|
|
49950
50080
|
proc.on("close", (code) => {
|
|
49951
50081
|
if (code !== 0) {
|
|
@@ -50035,6 +50165,7 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
|
50035
50165
|
phase: p.phase,
|
|
50036
50166
|
pct: p.pct,
|
|
50037
50167
|
detail: p.detail,
|
|
50168
|
+
log: p.log,
|
|
50038
50169
|
sessionId
|
|
50039
50170
|
}
|
|
50040
50171
|
});
|
|
@@ -50078,11 +50209,21 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
|
50078
50209
|
return result;
|
|
50079
50210
|
},
|
|
50080
50211
|
abortModelUpload: async (input) => (await this.getUploadStore()).abort(input),
|
|
50081
|
-
registerCustomModel: async (input) => this.registerCustomModel(input),
|
|
50212
|
+
registerCustomModel: async (input) => this.registerCustomModel(input, { ensureEngines: true }),
|
|
50213
|
+
ensureFormatsThenDistribute: async (input) => this.ensureFormatsThenDistribute_(input.modelId),
|
|
50214
|
+
ensureModelFormats: async (input) => this.ensureModelFormats_(input.modelId, input.formats),
|
|
50082
50215
|
removeCustomModel: async (input) => this.removeCustomModel(input),
|
|
50083
|
-
|
|
50216
|
+
removeModelFromNode: async (input) => this.removeModelFromNode(input),
|
|
50217
|
+
deleteSeedFromNode: async (input) => this.deleteSeedFromNode(input),
|
|
50218
|
+
listCustomModels: async () => {
|
|
50219
|
+
await this.pruneOrphanAvailability_();
|
|
50220
|
+
return this.listCustomModels();
|
|
50221
|
+
},
|
|
50084
50222
|
distributeModel: async (input) => this.distributeModel(input),
|
|
50085
|
-
listAvailability: async () =>
|
|
50223
|
+
listAvailability: async () => {
|
|
50224
|
+
await this.pruneOrphanAvailability_();
|
|
50225
|
+
return this.availability_.get();
|
|
50226
|
+
},
|
|
50086
50227
|
listFrigateCatalog: async () => FRIGATE_PUBLIC_CATALOG.map((c) => ({
|
|
50087
50228
|
id: c.id,
|
|
50088
50229
|
label: c.label,
|
|
@@ -50227,10 +50368,37 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
|
50227
50368
|
async distributeModel(input) {
|
|
50228
50369
|
const descriptor = (await this.state_.get()).find((m) => m.entry.id === input.modelId);
|
|
50229
50370
|
if (!descriptor) throw new Error(`distributeModel: model "${input.modelId}" is not registered`);
|
|
50371
|
+
const formatEntry = descriptor.entry.formats[input.format];
|
|
50372
|
+
if (formatEntry === void 0) throw new Error(`distributeModel: model "${input.modelId}" has no ${input.format} format in the Library`);
|
|
50230
50373
|
const api = this.ctx.api;
|
|
50231
50374
|
if (!api) throw new Error("distributeModel: ctx.api unavailable");
|
|
50375
|
+
if (/^https?:\/\//i.test(formatEntry.url)) {
|
|
50376
|
+
const downloaded = await api.pipelineExecutor.downloadModel.mutate({
|
|
50377
|
+
modelId: input.modelId,
|
|
50378
|
+
format: input.format,
|
|
50379
|
+
addonId: descriptor.stepId
|
|
50380
|
+
}, nodePin(input.nodeId));
|
|
50381
|
+
const bytes = Math.round(downloaded.sizeMB * 1e6);
|
|
50382
|
+
await this.availability_.update((prev) => upsertAvailability(prev, input.modelId, input.nodeId, {
|
|
50383
|
+
format: input.format,
|
|
50384
|
+
sha256: "",
|
|
50385
|
+
bytes,
|
|
50386
|
+
at: Date.now()
|
|
50387
|
+
}));
|
|
50388
|
+
this.ctx.logger.info("Downloaded custom model on node", { meta: {
|
|
50389
|
+
modelId: input.modelId,
|
|
50390
|
+
nodeId: input.nodeId,
|
|
50391
|
+
format: input.format,
|
|
50392
|
+
bytes
|
|
50393
|
+
} });
|
|
50394
|
+
return {
|
|
50395
|
+
ok: true,
|
|
50396
|
+
sha256: "",
|
|
50397
|
+
bytes
|
|
50398
|
+
};
|
|
50399
|
+
}
|
|
50232
50400
|
const res = await api.modelDistributor.distributeModel.mutate({
|
|
50233
|
-
|
|
50401
|
+
targetNodeId: input.nodeId,
|
|
50234
50402
|
modelId: input.modelId,
|
|
50235
50403
|
format: input.format,
|
|
50236
50404
|
entry: descriptor.entry
|
|
@@ -50309,59 +50477,49 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
|
50309
50477
|
}
|
|
50310
50478
|
}
|
|
50311
50479
|
/**
|
|
50312
|
-
*
|
|
50480
|
+
* Register one manifesto entry by id into the Library. Host/repo/revision
|
|
50481
|
+
* stay server-owned. Artifacts are NOT staged on the hub — optional
|
|
50482
|
+
* `targetNodeId` lands the HF URL on that node via download-on-target.
|
|
50313
50483
|
*/
|
|
50314
50484
|
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
|
-
});
|
|
50485
|
+
const entry = getScryptedManifestEntry(input.manifestId);
|
|
50486
|
+
if (!entry) throw new Error(`importScryptedCatalogModel: unknown manifest id "${input.manifestId}"`);
|
|
50487
|
+
let drift;
|
|
50488
|
+
try {
|
|
50489
|
+
const tree = await fetchScryptedRemoteTree({ fetchImpl: fetch });
|
|
50490
|
+
const row = compareScryptedManifestDrift(SCRYPTED_YOLO_MANIFEST.filter((e) => e.id === input.manifestId), tree)[0];
|
|
50491
|
+
drift = normalizeScryptedDrift(row ?? {
|
|
50492
|
+
manifestId: input.manifestId,
|
|
50493
|
+
status: "missing",
|
|
50494
|
+
details: "entry missing from manifesto"
|
|
50495
|
+
});
|
|
50496
|
+
} catch (err) {
|
|
50497
|
+
drift = {
|
|
50498
|
+
manifestId: input.manifestId,
|
|
50499
|
+
status: "unavailable",
|
|
50500
|
+
details: err instanceof Error ? err.message : "Hugging Face tree unavailable"
|
|
50501
|
+
};
|
|
50502
|
+
}
|
|
50503
|
+
if (drift.status !== "ok") throw new Error(`importScryptedCatalogModel: entry "${entry.id}" is blocked by drift (${drift.status}${drift.details ? `: ${drift.details}` : ""})`);
|
|
50504
|
+
const descriptor = buildScryptedRegistryDescriptor(entry);
|
|
50342
50505
|
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: {
|
|
50506
|
+
if (input.targetNodeId !== void 0 && input.targetNodeId.length > 0) await this.distributeModel({
|
|
50507
|
+
modelId: descriptor.entry.id,
|
|
50508
|
+
nodeId: input.targetNodeId,
|
|
50509
|
+
format: entry.format
|
|
50510
|
+
});
|
|
50511
|
+
this.ctx.logger.info("Registered Scrypted catalog model", { meta: {
|
|
50354
50512
|
manifestId: input.manifestId,
|
|
50355
50513
|
modelId: descriptor.entry.id,
|
|
50356
|
-
|
|
50514
|
+
format: entry.format,
|
|
50515
|
+
...input.targetNodeId !== void 0 ? { targetNodeId: input.targetNodeId } : {}
|
|
50357
50516
|
} });
|
|
50358
50517
|
return descriptor;
|
|
50359
50518
|
}
|
|
50360
50519
|
/**
|
|
50361
|
-
*
|
|
50362
|
-
*
|
|
50363
|
-
*
|
|
50364
|
-
* and record hub availability.
|
|
50520
|
+
* Register a curated PUBLIC Frigate catalog model into the Library. The
|
|
50521
|
+
* entry keeps its public URL — no hub staging. Optional `targetNodeId`
|
|
50522
|
+
* downloads on that node (same path as Library → Distribute).
|
|
50365
50523
|
*/
|
|
50366
50524
|
async importFrigateCatalogModel(input) {
|
|
50367
50525
|
const cat = FRIGATE_PUBLIC_CATALOG.find((c) => c.id === input.catalogId);
|
|
@@ -50370,29 +50528,21 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
|
50370
50528
|
...input.modelId !== void 0 ? { modelId: input.modelId } : {},
|
|
50371
50529
|
...input.name !== void 0 ? { name: input.name } : {}
|
|
50372
50530
|
});
|
|
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
50531
|
await this.registerCustomModel(descriptor);
|
|
50385
|
-
|
|
50386
|
-
|
|
50387
|
-
|
|
50388
|
-
|
|
50389
|
-
|
|
50390
|
-
|
|
50391
|
-
|
|
50532
|
+
let bytes = Math.round(cat.sizeMB * 1e6);
|
|
50533
|
+
if (input.targetNodeId !== void 0 && input.targetNodeId.length > 0) {
|
|
50534
|
+
const landed = await this.distributeModel({
|
|
50535
|
+
modelId: descriptor.entry.id,
|
|
50536
|
+
nodeId: input.targetNodeId,
|
|
50537
|
+
format: cat.format
|
|
50538
|
+
});
|
|
50539
|
+
bytes = landed.bytes > 0 ? landed.bytes : bytes;
|
|
50540
|
+
}
|
|
50541
|
+
this.ctx.logger.info("Registered Frigate catalog model", { meta: {
|
|
50392
50542
|
catalogId: input.catalogId,
|
|
50393
50543
|
modelId: descriptor.entry.id,
|
|
50394
|
-
|
|
50395
|
-
|
|
50544
|
+
...input.targetNodeId !== void 0 ? { targetNodeId: input.targetNodeId } : {},
|
|
50545
|
+
bytes
|
|
50396
50546
|
} });
|
|
50397
50547
|
return {
|
|
50398
50548
|
ok: true,
|
|
@@ -50408,11 +50558,20 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
|
50408
50558
|
* is hub-local: other nodes get it via Models → Distribute.
|
|
50409
50559
|
*/
|
|
50410
50560
|
async importFrigatePlusModel(input) {
|
|
50411
|
-
|
|
50561
|
+
const modelId = frigatePlusRegistryId(input.plusModelId);
|
|
50412
50562
|
const key = this.config.frigatePlusApiKey ?? "";
|
|
50413
50563
|
const { info, downloadUrl } = await requestPlusModelDownload(plusFetch, key, input.plusModelId);
|
|
50414
|
-
const
|
|
50415
|
-
const
|
|
50564
|
+
const typeTraits = plusTypeTraits(info.type);
|
|
50565
|
+
const traits = input.format === "tflite" ? {
|
|
50566
|
+
format: "tflite",
|
|
50567
|
+
extension: ".tflite",
|
|
50568
|
+
...info.type === "ssd" || info.type === "mobiledet" ? { postprocessor: "ssd" } : {}
|
|
50569
|
+
} : input.format === "onnx" ? {
|
|
50570
|
+
format: "onnx",
|
|
50571
|
+
extension: ".onnx",
|
|
50572
|
+
...typeTraits.postprocessor !== void 0 ? { postprocessor: typeTraits.postprocessor } : {}
|
|
50573
|
+
} : typeTraits;
|
|
50574
|
+
const filename = `${modelId}${traits.extension}`;
|
|
50416
50575
|
const modelsDir = await this.resolveModelsDir();
|
|
50417
50576
|
const destPath = node_path.default.join(modelsDir, filename);
|
|
50418
50577
|
await (0, _camstack_system_addon_utils.downloadFile)(downloadUrl, destPath);
|
|
@@ -50423,13 +50582,14 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
|
50423
50582
|
}
|
|
50424
50583
|
const sha256 = await ModelStudioAddon.fileSha256(destPath);
|
|
50425
50584
|
const descriptor = plusInfoToDescriptor(info, {
|
|
50426
|
-
modelId
|
|
50585
|
+
modelId,
|
|
50427
50586
|
name: input.name?.trim() || `Frigate+ ${info.type} (${input.plusModelId.slice(0, 8)})`,
|
|
50428
50587
|
filename,
|
|
50429
|
-
sizeMB: Number((bytes / 1e6).toFixed(1))
|
|
50588
|
+
sizeMB: Number((bytes / 1e6).toFixed(1)),
|
|
50589
|
+
...input.format !== void 0 ? { format: input.format } : {}
|
|
50430
50590
|
});
|
|
50431
|
-
await this.registerCustomModel(descriptor);
|
|
50432
|
-
await this.availability_.update((prev) => upsertAvailability(prev,
|
|
50591
|
+
await this.registerCustomModel(descriptor, { ensureEngines: false });
|
|
50592
|
+
await this.availability_.update((prev) => upsertAvailability(prev, modelId, "hub", {
|
|
50433
50593
|
format: traits.format,
|
|
50434
50594
|
sha256,
|
|
50435
50595
|
bytes,
|
|
@@ -50437,59 +50597,28 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
|
50437
50597
|
}));
|
|
50438
50598
|
this.ctx.logger.info("Imported Frigate+ model", { meta: {
|
|
50439
50599
|
plusModelId: input.plusModelId,
|
|
50440
|
-
modelId
|
|
50600
|
+
modelId,
|
|
50441
50601
|
type: info.type,
|
|
50442
|
-
bytes
|
|
50602
|
+
bytes,
|
|
50603
|
+
format: traits.format
|
|
50443
50604
|
} });
|
|
50605
|
+
const wanted = input.ensureFormats ?? [];
|
|
50444
50606
|
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
|
-
}
|
|
50607
|
+
if (traits.format === "onnx" && wanted.length > 0) {
|
|
50608
|
+
conversions = (await ensureEngineFormats(descriptor.entry, this.plusConversionDeps(async () => (await requestPlusModelDownload(plusFetch, key, input.plusModelId)).downloadUrl, descriptor.stepId), { targetFormats: wanted })).conversions;
|
|
50609
|
+
this.ctx.logger.info("Frigate+ import — engine conversions", { meta: {
|
|
50610
|
+
modelId,
|
|
50611
|
+
outcomes: conversions.map((o) => ({ ...o }))
|
|
50612
|
+
} });
|
|
50484
50613
|
}
|
|
50485
50614
|
return {
|
|
50486
50615
|
ok: true,
|
|
50487
|
-
modelId
|
|
50616
|
+
modelId,
|
|
50488
50617
|
bytes,
|
|
50489
50618
|
conversions
|
|
50490
50619
|
};
|
|
50491
50620
|
}
|
|
50492
|
-
async registerCustomModel(input) {
|
|
50621
|
+
async registerCustomModel(input, opts = {}) {
|
|
50493
50622
|
if (!input.stepId.trim()) throw new Error("registerCustomModel: stepId is required");
|
|
50494
50623
|
if (!input.entry.id.trim()) throw new Error("registerCustomModel: entry.id is required");
|
|
50495
50624
|
await this.state_.update((prev) => upsertCustomModel(prev, input));
|
|
@@ -50497,9 +50626,252 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
|
50497
50626
|
modelId: input.entry.id,
|
|
50498
50627
|
stepId: input.stepId
|
|
50499
50628
|
} });
|
|
50500
|
-
|
|
50629
|
+
let conversions = [];
|
|
50630
|
+
if (opts.ensureEngines === true && input.entry.formats.onnx !== void 0) {
|
|
50631
|
+
const onnxUrl = input.entry.formats.onnx.url;
|
|
50632
|
+
if (typeof onnxUrl === "string" && onnxUrl.length > 0) {
|
|
50633
|
+
const deps = this.plusConversionDeps(() => Promise.resolve(onnxUrl), input.stepId);
|
|
50634
|
+
conversions = (await ensureEngineFormats(input.entry, deps)).conversions;
|
|
50635
|
+
if (conversions.length > 0) this.ctx.logger.info("Custom model — engine conversions", { meta: {
|
|
50636
|
+
modelId: input.entry.id,
|
|
50637
|
+
outcomes: conversions.map((o) => ({ ...o }))
|
|
50638
|
+
} });
|
|
50639
|
+
}
|
|
50640
|
+
}
|
|
50641
|
+
return {
|
|
50642
|
+
ok: true,
|
|
50643
|
+
conversions
|
|
50644
|
+
};
|
|
50645
|
+
}
|
|
50646
|
+
plusConversionDeps(freshSourceUrl, stepId) {
|
|
50647
|
+
const api = this.ctx.api;
|
|
50648
|
+
return {
|
|
50649
|
+
recommend: (i) => recommendConvertNode(i, this.recommendDeps()),
|
|
50650
|
+
convert: async (convertInput, nId) => {
|
|
50651
|
+
if (!api) throw new Error("modelConvert unavailable");
|
|
50652
|
+
return api.modelConvert.convert.mutate(convertInput, nodePin(nId));
|
|
50653
|
+
},
|
|
50654
|
+
freshSourceUrl,
|
|
50655
|
+
upsertEntry: async (entry) => {
|
|
50656
|
+
await this.state_.update((prev) => upsertCustomModel(prev, {
|
|
50657
|
+
stepId,
|
|
50658
|
+
entry
|
|
50659
|
+
}));
|
|
50660
|
+
},
|
|
50661
|
+
markAvailable: async (nId, mId, artifacts) => {
|
|
50662
|
+
await this.availability_.update((prev) => {
|
|
50663
|
+
const seen = /* @__PURE__ */ new Set();
|
|
50664
|
+
let next = prev;
|
|
50665
|
+
for (const artifact of artifacts) {
|
|
50666
|
+
if (seen.has(artifact.format)) continue;
|
|
50667
|
+
seen.add(artifact.format);
|
|
50668
|
+
next = upsertAvailability(next, mId, nId, {
|
|
50669
|
+
format: artifact.format,
|
|
50670
|
+
sha256: "",
|
|
50671
|
+
bytes: Math.round(artifact.sizeMB * 1e6),
|
|
50672
|
+
at: Date.now()
|
|
50673
|
+
});
|
|
50674
|
+
}
|
|
50675
|
+
return next;
|
|
50676
|
+
});
|
|
50677
|
+
}
|
|
50678
|
+
};
|
|
50679
|
+
}
|
|
50680
|
+
async ensureModelFormats_(modelId, formats) {
|
|
50681
|
+
const row = (await this.state_.get()).find((m) => m.entry.id === modelId);
|
|
50682
|
+
if (row === void 0) throw new Error(`ensureModelFormats: model "${modelId}" is not registered`);
|
|
50683
|
+
const onnxUrl = row.entry.formats.onnx?.url;
|
|
50684
|
+
if (typeof onnxUrl !== "string" || onnxUrl.length === 0) throw new Error(`ensureModelFormats: model "${modelId}" has no ONNX to convert from`);
|
|
50685
|
+
const ensured = await ensureEngineFormats(row.entry, this.plusConversionDeps(async () => {
|
|
50686
|
+
const url = ((await this.state_.get()).find((m) => m.entry.id === modelId)?.entry)?.formats.onnx?.url ?? onnxUrl;
|
|
50687
|
+
if (typeof url !== "string" || url.length === 0) throw new Error(`ensureModelFormats: no ONNX url for "${modelId}"`);
|
|
50688
|
+
return url;
|
|
50689
|
+
}, row.stepId), { targetFormats: formats });
|
|
50690
|
+
this.ctx.logger.info("ensureModelFormats", { meta: {
|
|
50691
|
+
modelId,
|
|
50692
|
+
outcomes: ensured.conversions.map((o) => ({ ...o }))
|
|
50693
|
+
} });
|
|
50694
|
+
return { conversions: ensured.conversions };
|
|
50501
50695
|
}
|
|
50696
|
+
async ensureFormatsThenDistribute_(modelId) {
|
|
50697
|
+
const api = this.ctx.api;
|
|
50698
|
+
if (!api) throw new Error("ensureFormatsThenDistribute: ctx.api unavailable");
|
|
50699
|
+
const getEntry = async () => {
|
|
50700
|
+
return (await this.state_.get()).find((m) => m.entry.id === modelId)?.entry;
|
|
50701
|
+
};
|
|
50702
|
+
const entry = await getEntry();
|
|
50703
|
+
if (entry === void 0) throw new Error(`ensureFormatsThenDistribute: model "${modelId}" is not registered`);
|
|
50704
|
+
const onnxUrl = entry.formats.onnx?.url;
|
|
50705
|
+
const stepId = (await this.state_.get()).find((m) => m.entry.id === modelId)?.stepId ?? "object-detection";
|
|
50706
|
+
return ensureFormatsThenDistribute(modelId, {
|
|
50707
|
+
...this.plusConversionDeps(async () => {
|
|
50708
|
+
const url = (await getEntry())?.formats.onnx?.url ?? onnxUrl;
|
|
50709
|
+
if (typeof url !== "string" || url.length === 0) throw new Error(`ensureFormatsThenDistribute: no ONNX url for "${modelId}"`);
|
|
50710
|
+
return url;
|
|
50711
|
+
}, stepId),
|
|
50712
|
+
getEntry,
|
|
50713
|
+
listAgentNodeIds: async () => {
|
|
50714
|
+
return (await api.nodes.directory.query()).nodes.filter((n) => n.isOnline && !n.isHub).map((n) => n.id);
|
|
50715
|
+
},
|
|
50716
|
+
distribute: async (mId, nodeId, format) => {
|
|
50717
|
+
const res = await this.distributeModel({
|
|
50718
|
+
modelId: mId,
|
|
50719
|
+
nodeId,
|
|
50720
|
+
format
|
|
50721
|
+
});
|
|
50722
|
+
return {
|
|
50723
|
+
sha256: res.sha256,
|
|
50724
|
+
bytes: res.bytes
|
|
50725
|
+
};
|
|
50726
|
+
}
|
|
50727
|
+
});
|
|
50728
|
+
}
|
|
50729
|
+
/**
|
|
50730
|
+
* Delete one CamStack seed format on one node. Physically removes files from
|
|
50731
|
+
* that node's modelsDir via `pipelineExecutor.deleteModel` + `nodePin`.
|
|
50732
|
+
*/
|
|
50733
|
+
async deleteSeedFromNode(input) {
|
|
50734
|
+
const api = this.ctx.api;
|
|
50735
|
+
if (!api) throw new Error("deleteSeedFromNode: API not available");
|
|
50736
|
+
await api.pipelineExecutor.deleteModel.mutate({
|
|
50737
|
+
addonId: input.addonId,
|
|
50738
|
+
modelId: input.modelId,
|
|
50739
|
+
format: input.format
|
|
50740
|
+
}, nodePin(input.nodeId));
|
|
50741
|
+
this.ctx.logger.info("CamStack seed deleted from node disk", { meta: {
|
|
50742
|
+
modelId: input.modelId,
|
|
50743
|
+
format: input.format,
|
|
50744
|
+
nodeId: input.nodeId,
|
|
50745
|
+
addonId: input.addonId
|
|
50746
|
+
} });
|
|
50747
|
+
return { success: true };
|
|
50748
|
+
}
|
|
50749
|
+
/**
|
|
50750
|
+
* Delete one format on one node. Registry row stays; only that availability
|
|
50751
|
+
* chip is cleared. Pins are left alone — re-Send to node restores files.
|
|
50752
|
+
*/
|
|
50753
|
+
async removeModelFromNode(input) {
|
|
50754
|
+
const descriptor = (await this.state_.get()).find((m) => m.entry.id === input.modelId);
|
|
50755
|
+
if (!descriptor) throw new Error(`removeModelFromNode: model "${input.modelId}" is not registered`);
|
|
50756
|
+
if (descriptor.entry.formats[input.format] === void 0) throw new Error(`removeModelFromNode: model "${input.modelId}" has no ${input.format} format in the Library`);
|
|
50757
|
+
let deleted = false;
|
|
50758
|
+
const api = this.ctx.api;
|
|
50759
|
+
try {
|
|
50760
|
+
if (api) {
|
|
50761
|
+
await api.pipelineExecutor.deleteModel.mutate({
|
|
50762
|
+
addonId: descriptor.stepId,
|
|
50763
|
+
modelId: input.modelId,
|
|
50764
|
+
format: input.format
|
|
50765
|
+
}, nodePin(input.nodeId));
|
|
50766
|
+
deleted = true;
|
|
50767
|
+
}
|
|
50768
|
+
} catch (err) {
|
|
50769
|
+
this.ctx.logger.warn("removeModelFromNode: remote delete failed (continuing)", { meta: {
|
|
50770
|
+
modelId: input.modelId,
|
|
50771
|
+
nodeId: input.nodeId,
|
|
50772
|
+
format: input.format,
|
|
50773
|
+
error: err instanceof Error ? err.message : String(err)
|
|
50774
|
+
} });
|
|
50775
|
+
}
|
|
50776
|
+
if (input.nodeId === "hub") try {
|
|
50777
|
+
(0, _camstack_system_addon_utils.deleteModelFromDisk)(await this.resolveModelsDir(), descriptor.entry, input.format);
|
|
50778
|
+
deleted = true;
|
|
50779
|
+
} catch (err) {
|
|
50780
|
+
this.ctx.logger.warn("removeModelFromNode: hub disk wipe failed", { meta: {
|
|
50781
|
+
modelId: input.modelId,
|
|
50782
|
+
format: input.format,
|
|
50783
|
+
error: err instanceof Error ? err.message : String(err)
|
|
50784
|
+
} });
|
|
50785
|
+
}
|
|
50786
|
+
let clearedAvailability = false;
|
|
50787
|
+
await this.availability_.update((prev) => {
|
|
50788
|
+
const result = clearAvailabilityFormat(prev, input.modelId, input.nodeId, input.format);
|
|
50789
|
+
clearedAvailability = result.cleared;
|
|
50790
|
+
return result.next;
|
|
50791
|
+
});
|
|
50792
|
+
this.ctx.logger.info("Removed custom model format from node", { meta: {
|
|
50793
|
+
modelId: input.modelId,
|
|
50794
|
+
nodeId: input.nodeId,
|
|
50795
|
+
format: input.format,
|
|
50796
|
+
deleted,
|
|
50797
|
+
clearedAvailability
|
|
50798
|
+
} });
|
|
50799
|
+
return {
|
|
50800
|
+
deleted,
|
|
50801
|
+
clearedAvailability
|
|
50802
|
+
};
|
|
50803
|
+
}
|
|
50804
|
+
/**
|
|
50805
|
+
* Full purge of a Library model: delete artifacts on every node that had
|
|
50806
|
+
* them, clear availability (so Sources no longer shows "imported"), strip
|
|
50807
|
+
* pipeline pins that named this modelId (agents revert to hardware defaults
|
|
50808
|
+
* for that step), then drop the registry row.
|
|
50809
|
+
*/
|
|
50502
50810
|
async removeCustomModel(input) {
|
|
50811
|
+
const descriptor = (await this.state_.get()).find((m) => m.entry.id === input.modelId);
|
|
50812
|
+
if (!descriptor) {
|
|
50813
|
+
await this.availability_.update((prev) => clearAvailability(prev, input.modelId).next);
|
|
50814
|
+
return {
|
|
50815
|
+
removed: false,
|
|
50816
|
+
deletedNodes: [],
|
|
50817
|
+
clearedPins: 0
|
|
50818
|
+
};
|
|
50819
|
+
}
|
|
50820
|
+
const formats = Object.keys(descriptor.entry.formats).filter((f) => descriptor.entry.formats[f] !== void 0);
|
|
50821
|
+
const avail = await this.availability_.get();
|
|
50822
|
+
const nodeIds = new Set(["hub", ...Object.keys(avail[input.modelId] ?? {})]);
|
|
50823
|
+
const deletedNodes = [];
|
|
50824
|
+
const api = this.ctx.api;
|
|
50825
|
+
for (const nodeId of nodeIds) {
|
|
50826
|
+
let anyDeleted = false;
|
|
50827
|
+
for (const format of formats) try {
|
|
50828
|
+
if (api) {
|
|
50829
|
+
await api.pipelineExecutor.deleteModel.mutate({
|
|
50830
|
+
addonId: descriptor.stepId,
|
|
50831
|
+
modelId: input.modelId,
|
|
50832
|
+
format
|
|
50833
|
+
}, nodePin(nodeId));
|
|
50834
|
+
anyDeleted = true;
|
|
50835
|
+
}
|
|
50836
|
+
} catch (err) {
|
|
50837
|
+
this.ctx.logger.warn("removeCustomModel: remote delete failed (continuing)", { meta: {
|
|
50838
|
+
modelId: input.modelId,
|
|
50839
|
+
nodeId,
|
|
50840
|
+
format,
|
|
50841
|
+
error: err instanceof Error ? err.message : String(err)
|
|
50842
|
+
} });
|
|
50843
|
+
}
|
|
50844
|
+
if (anyDeleted) deletedNodes.push(nodeId);
|
|
50845
|
+
}
|
|
50846
|
+
try {
|
|
50847
|
+
const modelsDir = await this.resolveModelsDir();
|
|
50848
|
+
for (const format of formats) (0, _camstack_system_addon_utils.deleteModelFromDisk)(modelsDir, descriptor.entry, format);
|
|
50849
|
+
} catch (err) {
|
|
50850
|
+
this.ctx.logger.warn("removeCustomModel: hub disk wipe failed", { meta: {
|
|
50851
|
+
modelId: input.modelId,
|
|
50852
|
+
error: err instanceof Error ? err.message : String(err)
|
|
50853
|
+
} });
|
|
50854
|
+
}
|
|
50855
|
+
let clearedPins = 0;
|
|
50856
|
+
if (api) {
|
|
50857
|
+
try {
|
|
50858
|
+
clearedPins += await this.clearAgentModelPins(input.modelId);
|
|
50859
|
+
} catch (err) {
|
|
50860
|
+
this.ctx.logger.warn("removeCustomModel: agent pin clear failed", { meta: {
|
|
50861
|
+
modelId: input.modelId,
|
|
50862
|
+
error: err instanceof Error ? err.message : String(err)
|
|
50863
|
+
} });
|
|
50864
|
+
}
|
|
50865
|
+
try {
|
|
50866
|
+
clearedPins += await this.clearCameraModelPins(input.modelId, descriptor.stepId);
|
|
50867
|
+
} catch (err) {
|
|
50868
|
+
this.ctx.logger.warn("removeCustomModel: camera pin clear failed", { meta: {
|
|
50869
|
+
modelId: input.modelId,
|
|
50870
|
+
error: err instanceof Error ? err.message : String(err)
|
|
50871
|
+
} });
|
|
50872
|
+
}
|
|
50873
|
+
}
|
|
50874
|
+
await this.availability_.update((prev) => clearAvailability(prev, input.modelId).next);
|
|
50503
50875
|
let removed = false;
|
|
50504
50876
|
await this.state_.update((prev) => {
|
|
50505
50877
|
const result = removeCustomModel(prev, input.modelId);
|
|
@@ -50508,13 +50880,80 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
|
50508
50880
|
});
|
|
50509
50881
|
this.ctx.logger.info("Removed custom model", { meta: {
|
|
50510
50882
|
modelId: input.modelId,
|
|
50511
|
-
removed
|
|
50883
|
+
removed,
|
|
50884
|
+
deletedNodes,
|
|
50885
|
+
clearedPins
|
|
50512
50886
|
} });
|
|
50513
|
-
return {
|
|
50887
|
+
return {
|
|
50888
|
+
removed,
|
|
50889
|
+
deletedNodes,
|
|
50890
|
+
clearedPins
|
|
50891
|
+
};
|
|
50892
|
+
}
|
|
50893
|
+
/** Strip `modelId` from every agent's inferenceDevices step pins. */
|
|
50894
|
+
async clearAgentModelPins(modelId) {
|
|
50895
|
+
const api = this.ctx.api;
|
|
50896
|
+
if (!api) return 0;
|
|
50897
|
+
const agents = await api.pipelineOrchestrator.listAgentSettings.query();
|
|
50898
|
+
let cleared = 0;
|
|
50899
|
+
for (const agent of agents) {
|
|
50900
|
+
const devices = agent.settings.inferenceDevices;
|
|
50901
|
+
if (devices === void 0) continue;
|
|
50902
|
+
const { next, cleared: n } = stripModelIdFromInferenceDevices(devices, modelId);
|
|
50903
|
+
if (n === 0) continue;
|
|
50904
|
+
await api.pipelineOrchestrator.setAgentInferenceDevices.mutate({
|
|
50905
|
+
agentNodeId: agent.nodeId,
|
|
50906
|
+
inferenceDevices: next
|
|
50907
|
+
});
|
|
50908
|
+
cleared += n;
|
|
50909
|
+
}
|
|
50910
|
+
return cleared;
|
|
50911
|
+
}
|
|
50912
|
+
/**
|
|
50913
|
+
* Clear per-camera step overrides that pin `modelId` for `stepId`.
|
|
50914
|
+
* `patch: null` drops the override so the camera inherits the agent default.
|
|
50915
|
+
*/
|
|
50916
|
+
async clearCameraModelPins(modelId, stepId) {
|
|
50917
|
+
const api = this.ctx.api;
|
|
50918
|
+
if (!api) return 0;
|
|
50919
|
+
const devices = await api.deviceManager.listAll.query({ projection: "slim" });
|
|
50920
|
+
let cleared = 0;
|
|
50921
|
+
for (const device of devices) {
|
|
50922
|
+
const overrides = await api.pipelineOrchestrator.getCameraStepOverrides.query({ deviceId: device.id });
|
|
50923
|
+
if (overrides === null) continue;
|
|
50924
|
+
for (const [agentNodeId, byDeviceKey] of Object.entries(overrides)) for (const [deviceKey, byStep] of Object.entries(byDeviceKey)) {
|
|
50925
|
+
if (byStep[stepId]?.modelId !== modelId) continue;
|
|
50926
|
+
await api.pipelineOrchestrator.setCameraStepOverride.mutate({
|
|
50927
|
+
deviceId: device.id,
|
|
50928
|
+
agentNodeId,
|
|
50929
|
+
deviceKey,
|
|
50930
|
+
addonId: stepId,
|
|
50931
|
+
patch: null
|
|
50932
|
+
});
|
|
50933
|
+
cleared += 1;
|
|
50934
|
+
}
|
|
50935
|
+
}
|
|
50936
|
+
return cleared;
|
|
50514
50937
|
}
|
|
50515
50938
|
async listCustomModels() {
|
|
50516
50939
|
return this.state_.get();
|
|
50517
50940
|
}
|
|
50941
|
+
/**
|
|
50942
|
+
* Self-heal: drop availability rows for modelIds no longer in the Library
|
|
50943
|
+
* registry. Import and Library both trigger this so an orphan cannot keep
|
|
50944
|
+
* Sources stuck on "imported" after a registry wipe.
|
|
50945
|
+
*/
|
|
50946
|
+
async pruneOrphanAvailability_() {
|
|
50947
|
+
try {
|
|
50948
|
+
const [avail, models] = await Promise.all([this.availability_.get(), this.state_.get()]);
|
|
50949
|
+
const { next, pruned } = pruneOrphanAvailability(avail, new Set(models.map((m) => m.entry.id)));
|
|
50950
|
+
if (pruned.length === 0) return;
|
|
50951
|
+
await this.availability_.update(() => next);
|
|
50952
|
+
this.ctx.logger.info("Pruned orphan model availability", { meta: { pruned } });
|
|
50953
|
+
} catch (err) {
|
|
50954
|
+
this.ctx.logger.warn("pruneOrphanAvailability failed (continuing)", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
50955
|
+
}
|
|
50956
|
+
}
|
|
50518
50957
|
};
|
|
50519
50958
|
//#endregion
|
|
50520
50959
|
exports.ModelStudioAddon = ModelStudioAddon;
|