@camstack/addon-model-studio 1.1.198 → 1.1.200

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