@camstack/addon-model-studio 1.1.199 → 1.1.201

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