@camstack/addon-model-studio 1.1.199 → 1.1.201

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