@camstack/addon-model-studio 1.1.199 → 1.1.200

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (20) hide show
  1. package/THIRD_PARTY_MODELS.md +6 -30
  2. package/dist/{MotionZonesSettings-JRWr-847.mjs → MotionZonesSettings-DXWpYKPD.mjs} +2 -2
  3. package/dist/{PrivacyMaskSettings-BfpUuDZo.mjs → PrivacyMaskSettings-BcaCmwR2.mjs} +4 -4
  4. package/dist/{SceneMonitorEditor-B6q_snqj.mjs → SceneMonitorEditor-D8IoPtTi.mjs} +3 -3
  5. package/dist/_stub.js +9033 -6124
  6. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-BNn0AWfL.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-ChSw1hrV.mjs} +3 -3
  7. package/dist/_virtual_mf___mfe_internal__addon_model_studio_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-C0rOztYh.mjs +26 -0
  8. package/dist/addon-model-studio.css +1 -1
  9. package/dist/{hostInit-CnCPORCz.mjs → hostInit-TRVWPuo-.mjs} +2 -2
  10. package/dist/model-studio.addon.js +870 -431
  11. package/dist/model-studio.addon.mjs +870 -436
  12. package/dist/{player-overlays-RAKUtQ68.mjs → player-overlays-4Vb0Si7c.mjs} +2 -2
  13. package/dist/remoteEntry.js +1 -1
  14. package/dist/{responsive-Cr8pn1rO.mjs → responsive-D8JojVmZ.mjs} +1 -1
  15. package/dist/{scene-monitor-copy-NIVM76QJ.mjs → scene-monitor-copy-Cw3r5D6b.mjs} +1 -1
  16. package/dist/{square-BOlfCWYp.mjs → square-C4Y02RV4.mjs} +1 -1
  17. package/dist/{trash-2-aAfBxjgh.mjs → trash-2-D-gDRhvd.mjs} +1 -1
  18. package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-yi-EovNM.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-Bs1AQFNt.mjs} +1 -1
  19. package/package.json +2 -1
  20. package/dist/_virtual_mf___mfe_internal__addon_model_studio_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BKQUo9FB.mjs +0 -26
@@ -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`. */
@@ -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
@@ -21662,39 +21481,19 @@ Object.freeze(Object.fromEntries([{
21662
21481
  label: "Face recognition model",
21663
21482
  defaultModelId: "auraface-r100",
21664
21483
  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
- ]
21484
+ options: [{
21485
+ id: "auraface-r100",
21486
+ label: "AuraFace R100"
21487
+ }]
21679
21488
  }, {
21680
21489
  stepId: "clip-embedding",
21681
21490
  label: "Semantic search model",
21682
- defaultModelId: "mobileclip-s1",
21491
+ defaultModelId: "siglip2-b16-224",
21683
21492
  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
- ]
21493
+ options: [{
21494
+ id: "siglip2-b16-224",
21495
+ label: "SigLIP2 B/16 (Apache-2.0)"
21496
+ }]
21698
21497
  }].map((s) => [s.stepId, s.defaultModelId])));
21699
21498
  var CLUSTER_STEP_SETTING_FIELDS = [{
21700
21499
  stepId: "face-embedding",
@@ -32406,7 +32205,13 @@ var OsdStatusSchema = object({ overlays: array(object({
32406
32205
  */
32407
32206
  availablePositions: array(OsdPositionEnum).readonly(),
32408
32207
  /** Firmware does not allow editing the `text` field (e.g. timestamp). */
32409
- readOnly: boolean().optional()
32208
+ readOnly: boolean().optional(),
32209
+ /**
32210
+ * Firmware text ceiling for this overlay. Absent = no known limit.
32211
+ * The OSD manager truncates here on every write — never stores its own
32212
+ * copy on the binding.
32213
+ */
32214
+ maxCharacters: number().int().positive().optional()
32410
32215
  })).readonly() });
32411
32216
  var OsdOverlayPatchSchema = object({
32412
32217
  enabled: boolean().optional(),
@@ -32457,7 +32262,9 @@ var OsdTemplateBindingSchema = object({
32457
32262
  valuePath: string().min(1).max(64),
32458
32263
  /** Convert TO this unit at render (`tryConvertUnit`). Absent = source unit. */
32459
32264
  displayUnit: string().max(16).optional(),
32460
- maxDecimals: number().int().min(0).max(4).default(1)
32265
+ maxDecimals: number().int().min(0).max(4).default(1),
32266
+ /** Raw value → display text (same shape as slot binding). Empty string hides. */
32267
+ valueMap: record(string(), string()).optional()
32461
32268
  });
32462
32269
  /**
32463
32270
  * A named template: bindings (alias → leaf field) + a parser that
@@ -32507,18 +32314,11 @@ var OsdSlotBindingSchema = object({
32507
32314
  source: OsdSourceSchema,
32508
32315
  /** `${value}` and `${unit}` are substituted; every occurrence. */
32509
32316
  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
32317
  /**
32513
32318
  * Decimal places for a numeric value. `0` yields an integer — the
32514
32319
  * documented workaround for firmwares that reject `.` in overlay text.
32515
32320
  */
32516
32321
  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
32322
  /** Raw value → display text, e.g. `{"true":"MOTION","false":""}`. */
32523
32323
  valueMap: record(string(), string()).optional(),
32524
32324
  /** Time windows in which the slot is shown. Absent = always. */
@@ -32527,9 +32327,7 @@ var OsdSlotBindingSchema = object({
32527
32327
  * Display gate, in the notification centre's condition vocabulary.
32528
32328
  * Only the keys reported by `getConditionSupport` are accepted.
32529
32329
  */
32530
- conditions: NcConditionsSchema.optional(),
32531
- /** Rendered when the gate is closed or the value unreadable. Empty = hide. */
32532
- fallbackText: string().max(64).default("")
32330
+ conditions: NcConditionsSchema.optional()
32533
32331
  });
32534
32332
  /** One camera slot, as the operator sees it: firmware truth + our binding. */
32535
32333
  var OsdSlotViewSchema = object({
@@ -48501,6 +48299,8 @@ var STATE_SUMMARY_VARS = [
48501
48299
  [...RULE_TEXT],
48502
48300
  [...RULE_TEXT],
48503
48301
  [...RULE_TEXT],
48302
+ [...RULE_TEXT],
48303
+ [...RULE_TEXT],
48504
48304
  ...STATE_SUMMARY_VARS
48505
48305
  ];
48506
48306
  new Set([
@@ -49081,7 +48881,9 @@ async function convertModel(input, deps) {
49081
48881
  var ConvertProgressSchema = object({
49082
48882
  phase: string(),
49083
48883
  pct: number().optional(),
49084
- detail: string().optional()
48884
+ detail: string().optional(),
48885
+ /** Raw process line (stderr) — UI log pane, not a phase step. */
48886
+ log: string().optional()
49085
48887
  });
49086
48888
  var ConvertResultLineSchema = object({ result: object({ artifacts: array(ConvertArtifactSchema) }) });
49087
48889
  function buildEntry(metadata, artifacts) {
@@ -49150,29 +48952,57 @@ function buildModelConvertProvider(deps) {
49150
48952
  //#endregion
49151
48953
  //#region src/model-registry.ts
49152
48954
  /**
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).
48955
+ * Record that `modelId` now has `format` on `nodeId`. Merges into the node's
48956
+ * formats map — never drops a sibling format on the same node.
49155
48957
  */
49156
- function upsertAvailability(map, modelId, nodeId, entry) {
48958
+ function upsertAvailabilityFormat(map, modelId, nodeId, format, presence) {
48959
+ const prevNode = map[modelId]?.[nodeId] ?? { formats: {} };
49157
48960
  return {
49158
48961
  ...map,
49159
48962
  [modelId]: {
49160
48963
  ...map[modelId],
49161
- [nodeId]: entry
48964
+ [nodeId]: { formats: {
48965
+ ...prevNode.formats,
48966
+ [format]: presence
48967
+ } }
49162
48968
  }
49163
48969
  };
49164
48970
  }
49165
48971
  /**
48972
+ * @deprecated Prefer {@link upsertAvailabilityFormat}. Kept for call sites that
48973
+ * still pass a single-format legacy shape — folds into multi-format.
48974
+ */
48975
+ function upsertAvailability(map, modelId, nodeId, entry) {
48976
+ return upsertAvailabilityFormat(map, modelId, nodeId, entry.format, {
48977
+ sha256: entry.sha256,
48978
+ bytes: entry.bytes,
48979
+ at: entry.at
48980
+ });
48981
+ }
48982
+ /**
49166
48983
  * Pure registry operations over the custom-model list. Kept side-effect-free
49167
48984
  * so they unit-test without a live `BaseAddon` context — the addon class is a
49168
48985
  * thin durable-state wrapper around these.
49169
48986
  */
49170
48987
  /**
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.
48988
+ * Insert or replace a descriptor keyed by `entry.id`. When the id already
48989
+ * exists, **formats are merged** (Scrypted multi-format accumulate) — sibling
48990
+ * formats on the prior entry are never dropped. Returns a new array.
49173
48991
  */
49174
48992
  function upsertCustomModel(list, descriptor) {
49175
- return [...list.filter((m) => m.entry.id !== descriptor.entry.id), descriptor];
48993
+ const prev = list.find((m) => m.entry.id === descriptor.entry.id);
48994
+ const merged = prev === void 0 ? descriptor : {
48995
+ stepId: descriptor.stepId,
48996
+ entry: {
48997
+ ...prev.entry,
48998
+ ...descriptor.entry,
48999
+ formats: {
49000
+ ...prev.entry.formats,
49001
+ ...descriptor.entry.formats
49002
+ }
49003
+ }
49004
+ };
49005
+ return [...list.filter((m) => m.entry.id !== descriptor.entry.id), merged];
49176
49006
  }
49177
49007
  /** Drop the descriptor with the given `entry.id`. Reports whether anything changed. */
49178
49008
  function removeCustomModel(list, modelId) {
@@ -49182,6 +49012,114 @@ function removeCustomModel(list, modelId) {
49182
49012
  removed: next.length !== list.length
49183
49013
  };
49184
49014
  }
49015
+ /** Drop every node entry for `modelId` from the availability map. */
49016
+ function clearAvailability(map, modelId) {
49017
+ if (map[modelId] === void 0) return {
49018
+ next: map,
49019
+ hadEntry: false
49020
+ };
49021
+ const next = { ...map };
49022
+ delete next[modelId];
49023
+ return {
49024
+ next,
49025
+ hadEntry: true
49026
+ };
49027
+ }
49028
+ /**
49029
+ * Drop one `(modelId, nodeId, format)` from availability. Empty nodes and empty
49030
+ * model rows are pruned so Library "On nodes" does not keep a ghost chip.
49031
+ */
49032
+ function clearAvailabilityFormat(map, modelId, nodeId, format) {
49033
+ const perNode = map[modelId];
49034
+ if (perNode === void 0) return {
49035
+ next: map,
49036
+ cleared: false
49037
+ };
49038
+ const node = perNode[nodeId];
49039
+ if (node === void 0 || node.formats[format] === void 0) return {
49040
+ next: map,
49041
+ cleared: false
49042
+ };
49043
+ const formats = { ...node.formats };
49044
+ delete formats[format];
49045
+ const nextNodes = { ...perNode };
49046
+ if (Object.keys(formats).length === 0) delete nextNodes[nodeId];
49047
+ else nextNodes[nodeId] = { formats };
49048
+ if (Object.keys(nextNodes).length === 0) {
49049
+ const next = { ...map };
49050
+ delete next[modelId];
49051
+ return {
49052
+ next,
49053
+ cleared: true
49054
+ };
49055
+ }
49056
+ return {
49057
+ next: {
49058
+ ...map,
49059
+ [modelId]: nextNodes
49060
+ },
49061
+ cleared: true
49062
+ };
49063
+ }
49064
+ /**
49065
+ * Drop availability rows whose modelId is not in the Library registry.
49066
+ * Orphans left Sources showing "imported" after a registry-only wipe.
49067
+ */
49068
+ function pruneOrphanAvailability(map, registeredIds) {
49069
+ const pruned = [];
49070
+ let next = null;
49071
+ for (const modelId of Object.keys(map)) {
49072
+ if (registeredIds.has(modelId)) continue;
49073
+ if (next === null) next = { ...map };
49074
+ delete next[modelId];
49075
+ pruned.push(modelId);
49076
+ }
49077
+ return {
49078
+ next: next ?? map,
49079
+ pruned
49080
+ };
49081
+ }
49082
+ function stripModelIdFromInferenceDevices(devices, modelId) {
49083
+ let cleared = 0;
49084
+ const next = {};
49085
+ for (const [key, device] of Object.entries(devices)) {
49086
+ const stepsIn = device.steps;
49087
+ if (stepsIn === void 0) {
49088
+ next[key] = {
49089
+ enabled: device.enabled,
49090
+ ...device.weight !== void 0 ? { weight: device.weight } : {},
49091
+ ...device.maxSessions !== void 0 ? { maxSessions: device.maxSessions } : {}
49092
+ };
49093
+ continue;
49094
+ }
49095
+ const stepsOut = {};
49096
+ for (const [stepId, step] of Object.entries(stepsIn)) {
49097
+ if (step.modelId === modelId) {
49098
+ cleared += 1;
49099
+ const kept = {};
49100
+ if (step.settings !== void 0) kept.settings = { ...step.settings };
49101
+ if (step.jumpDeviceKey !== void 0) kept.jumpDeviceKey = step.jumpDeviceKey;
49102
+ if (Object.keys(kept).length > 0) stepsOut[stepId] = kept;
49103
+ continue;
49104
+ }
49105
+ stepsOut[stepId] = {
49106
+ ...step.modelId !== void 0 ? { modelId: step.modelId } : {},
49107
+ ...step.settings !== void 0 ? { settings: { ...step.settings } } : {},
49108
+ ...step.jumpDeviceKey !== void 0 ? { jumpDeviceKey: step.jumpDeviceKey } : {}
49109
+ };
49110
+ }
49111
+ next[key] = {
49112
+ enabled: device.enabled,
49113
+ ...device.weight !== void 0 ? { weight: device.weight } : {},
49114
+ ...device.maxSessions !== void 0 ? { maxSessions: device.maxSessions } : {},
49115
+ ...Object.keys(stepsOut).length > 0 ? { steps: stepsOut } : {}
49116
+ };
49117
+ }
49118
+ return {
49119
+ next,
49120
+ cleared
49121
+ };
49122
+ }
49185
49123
  //#endregion
49186
49124
  //#region src/frigate/frigate-import.ts
49187
49125
  /**
@@ -49417,6 +49355,64 @@ async function runPlusConversions(deps, args) {
49417
49355
  return outcomes;
49418
49356
  }
49419
49357
  //#endregion
49358
+ //#region src/ensure-formats.ts
49359
+ var ENGINE_FORMATS = ["openvino", "coreml"];
49360
+ function missingEngineFormats(entry) {
49361
+ return ENGINE_FORMATS.filter((f) => entry.formats[f] === void 0);
49362
+ }
49363
+ async function ensureEngineFormats(entry, deps, opts = {}) {
49364
+ if (entry.formats.onnx === void 0) return {
49365
+ entry,
49366
+ conversions: []
49367
+ };
49368
+ const missing = (opts.targetFormats ?? ENGINE_FORMATS).filter((f) => entry.formats[f] === void 0);
49369
+ if (missing.length === 0) return {
49370
+ entry,
49371
+ conversions: []
49372
+ };
49373
+ return {
49374
+ entry,
49375
+ conversions: await runPlusConversions(deps, {
49376
+ entry,
49377
+ targetFormats: missing
49378
+ })
49379
+ };
49380
+ }
49381
+ /**
49382
+ * Ensure OV/CoreML from ONNX when missing, then push every declared format to
49383
+ * every agent (hub is skipped — it already hosts imports).
49384
+ */
49385
+ async function ensureFormatsThenDistribute(modelId, deps) {
49386
+ const initial = await deps.getEntry();
49387
+ if (initial === void 0) throw new Error(`ensureFormatsThenDistribute: model "${modelId}" is not registered`);
49388
+ let conversions = [];
49389
+ if (initial.formats.onnx !== void 0 && missingEngineFormats(initial).length > 0) conversions = (await ensureEngineFormats(initial, deps)).conversions;
49390
+ const entry = await deps.getEntry() ?? initial;
49391
+ const formats = Object.keys(entry.formats).filter((f) => entry.formats[f] !== void 0);
49392
+ const agents = await deps.listAgentNodeIds();
49393
+ const distributed = [];
49394
+ for (const nodeId of agents) for (const format of formats) try {
49395
+ const res = await deps.distribute(modelId, nodeId, format);
49396
+ distributed.push({
49397
+ nodeId,
49398
+ format,
49399
+ ok: true,
49400
+ bytes: res.bytes
49401
+ });
49402
+ } catch (err) {
49403
+ distributed.push({
49404
+ nodeId,
49405
+ format,
49406
+ ok: false,
49407
+ error: err instanceof Error ? err.message : String(err)
49408
+ });
49409
+ }
49410
+ return {
49411
+ conversions,
49412
+ distributed
49413
+ };
49414
+ }
49415
+ //#endregion
49420
49416
  //#region src/frigate/frigate-plus.ts
49421
49417
  var PLUS_API_HOST = "https://api.frigate.video";
49422
49418
  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 +49516,12 @@ var PLUS_TYPE_MAP = {
49520
49516
  postprocessor: "ssd",
49521
49517
  extension: ".tflite"
49522
49518
  },
49519
+ /** List API uses `ssd` for MobileDet rows. */
49520
+ ssd: {
49521
+ format: "tflite",
49522
+ postprocessor: "ssd",
49523
+ extension: ".tflite"
49524
+ },
49523
49525
  yolonas: {
49524
49526
  format: "onnx",
49525
49527
  postprocessor: "yolonas",
@@ -49528,6 +49530,10 @@ var PLUS_TYPE_MAP = {
49528
49530
  yolov9: {
49529
49531
  format: "onnx",
49530
49532
  extension: ".onnx"
49533
+ },
49534
+ "yolo-generic": {
49535
+ format: "onnx",
49536
+ extension: ".onnx"
49531
49537
  }
49532
49538
  };
49533
49539
  function plusTypeTraits(type) {
@@ -49552,7 +49558,16 @@ function orderedLabels(labelMap) {
49552
49558
  * Distribute.
49553
49559
  */
49554
49560
  function plusInfoToDescriptor(info, opts) {
49555
- const traits = plusTypeTraits(info.type);
49561
+ const base = plusTypeTraits(info.type);
49562
+ const traits = opts.format === "tflite" ? {
49563
+ format: "tflite",
49564
+ extension: ".tflite",
49565
+ ...info.type === "ssd" || info.type === "mobiledet" ? { postprocessor: "ssd" } : {}
49566
+ } : opts.format === "onnx" ? {
49567
+ format: "onnx",
49568
+ extension: ".onnx",
49569
+ ...base.postprocessor !== void 0 ? { postprocessor: base.postprocessor } : {}
49570
+ } : base;
49556
49571
  const labels = orderedLabels(info.labelMap);
49557
49572
  return {
49558
49573
  stepId: "object-detection",
@@ -49670,15 +49685,33 @@ var ModelUploadStore = class {
49670
49685
  * only the read-array `listModels` (auto-concatenated across providers); the
49671
49686
  * mutations live here so the cap stays a clean collection.
49672
49687
  */
49673
- /** One node's copy of a model: which format landed there and its digest. */
49674
- var ModelNodeAvailabilitySchema = object({
49675
- format: _enum(MODEL_FORMATS),
49688
+ /** One format's presence on a node. */
49689
+ var ModelFormatPresenceSchema = object({
49676
49690
  sha256: string(),
49677
49691
  bytes: number(),
49678
49692
  at: number()
49679
49693
  });
49680
- /** `modelId → nodeId → availability`. */
49694
+ /**
49695
+ * Per-node availability — multi-format. Legacy durable rows were
49696
+ * `{ format, sha256, bytes, at }`; preprocess folds them into `formats`.
49697
+ */
49698
+ var ModelNodeAvailabilitySchema = preprocess((raw) => {
49699
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return raw;
49700
+ const rec = raw;
49701
+ if (typeof rec.formats === "object" && rec.formats !== null && !Array.isArray(rec.formats)) return { formats: rec.formats };
49702
+ if (typeof rec.format === "string") return { formats: { [rec.format]: {
49703
+ sha256: typeof rec.sha256 === "string" ? rec.sha256 : "",
49704
+ bytes: typeof rec.bytes === "number" ? rec.bytes : 0,
49705
+ at: typeof rec.at === "number" ? rec.at : 0
49706
+ } } };
49707
+ return raw;
49708
+ }, object({ formats: partialRecord(_enum(MODEL_FORMATS), ModelFormatPresenceSchema) }));
49709
+ /** `modelId → nodeId → { formats: { [format]: presence } }`. */
49681
49710
  var ModelAvailabilityMapSchema = record(string(), record(string(), ModelNodeAvailabilitySchema));
49711
+ /** Stable Library id for a Frigate+ training (chip id ↔ registry). */
49712
+ function frigatePlusRegistryId(plusModelId) {
49713
+ return `frigate-plus-${plusModelId}`;
49714
+ }
49682
49715
  var ScryptedManifestFileSchema = object({
49683
49716
  relativePath: string(),
49684
49717
  size: number(),
@@ -49751,8 +49784,75 @@ var FrigatePlusModelSchema = object({
49751
49784
  height: number().optional()
49752
49785
  });
49753
49786
  var modelStudioActions = defineCustomActions({
49754
- registerCustomModel: customAction(CustomModelDescriptorSchema, object({ ok: literal(true) }), { kind: "mutation" }),
49755
- removeCustomModel: customAction(object({ modelId: string() }), object({ removed: boolean() }), { kind: "mutation" }),
49787
+ registerCustomModel: customAction(CustomModelDescriptorSchema, object({
49788
+ ok: literal(true),
49789
+ conversions: array(object({
49790
+ format: _enum(["openvino", "coreml"]),
49791
+ ok: boolean(),
49792
+ nodeId: string().optional(),
49793
+ error: string().optional()
49794
+ })).readonly()
49795
+ }), { kind: "mutation" }),
49796
+ /**
49797
+ * Ensure OV/CoreML from ONNX when missing, then distribute every declared
49798
+ * format to every online agent (hub skipped).
49799
+ */
49800
+ ensureFormatsThenDistribute: customAction(object({ modelId: string() }), object({
49801
+ conversions: array(object({
49802
+ format: _enum(["openvino", "coreml"]),
49803
+ ok: boolean(),
49804
+ nodeId: string().optional(),
49805
+ error: string().optional()
49806
+ })).readonly(),
49807
+ distributed: array(object({
49808
+ nodeId: string(),
49809
+ format: _enum(MODEL_FORMATS),
49810
+ ok: boolean(),
49811
+ error: string().optional(),
49812
+ bytes: number().optional()
49813
+ })).readonly()
49814
+ }), { kind: "mutation" }),
49815
+ /**
49816
+ * Convert missing OpenVINO / CoreML from an already-registered ONNX entry
49817
+ * (Frigate+ / Custom per-format Import). Only the requested formats run.
49818
+ */
49819
+ ensureModelFormats: customAction(object({
49820
+ modelId: string(),
49821
+ formats: array(_enum(["openvino", "coreml"])).min(1).readonly()
49822
+ }), object({ conversions: array(object({
49823
+ format: _enum(["openvino", "coreml"]),
49824
+ ok: boolean(),
49825
+ nodeId: string().optional(),
49826
+ error: string().optional()
49827
+ })).readonly() }), { kind: "mutation" }),
49828
+ removeCustomModel: customAction(object({ modelId: string() }), object({
49829
+ removed: boolean(),
49830
+ deletedNodes: array(string()).readonly(),
49831
+ clearedPins: number().int().nonnegative()
49832
+ }), { kind: "mutation" }),
49833
+ /**
49834
+ * Delete one format of a Library model from one node. Keeps the registry row
49835
+ * and sibling formats/nodes; clears only that availability chip.
49836
+ */
49837
+ removeModelFromNode: customAction(object({
49838
+ modelId: string(),
49839
+ nodeId: string(),
49840
+ format: _enum(MODEL_FORMATS)
49841
+ }), object({
49842
+ deleted: boolean(),
49843
+ clearedAvailability: boolean()
49844
+ }), { kind: "mutation" }),
49845
+ /**
49846
+ * Wipe one CamStack seed format from a node's modelsDir (Installed Remove).
49847
+ * Routes via `nodePin` — `pipelineExecutor.deleteModel` has no `nodeId` in
49848
+ * its input schema, so a bare tRPC mutate always hit the hub.
49849
+ */
49850
+ deleteSeedFromNode: customAction(object({
49851
+ addonId: string(),
49852
+ modelId: string(),
49853
+ format: _enum(MODEL_FORMATS),
49854
+ nodeId: string()
49855
+ }), object({ success: literal(true) }), { kind: "mutation" }),
49756
49856
  listCustomModels: customAction(_void(), array(CustomModelDescriptorSchema).readonly()),
49757
49857
  /**
49758
49858
  * Distribute a registered model's FORMAT (resident on the hub) to a target
@@ -49792,17 +49892,16 @@ var modelStudioActions = defineCustomActions({
49792
49892
  }), { kind: "mutation" }),
49793
49893
  abortModelUpload: customAction(object({ uploadId: string() }), object({ ok: literal(true) }), { kind: "mutation" }),
49794
49894
  /**
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).
49895
+ * Curated PUBLIC Frigate model catalog (D214 addendum). Import registers the
49896
+ * descriptor with its public URL (Library only). Optional `targetNodeId`
49897
+ * downloads on that node — no hub staging disk.
49800
49898
  */
49801
49899
  listFrigateCatalog: customAction(_void(), array(FrigateCatalogEntrySchema).readonly()),
49802
49900
  importFrigateCatalogModel: customAction(object({
49803
49901
  catalogId: string(),
49804
49902
  modelId: string().optional(),
49805
- name: string().optional()
49903
+ name: string().optional(),
49904
+ targetNodeId: string().optional()
49806
49905
  }), object({
49807
49906
  ok: literal(true),
49808
49907
  modelId: string(),
@@ -49811,13 +49910,17 @@ var modelStudioActions = defineCustomActions({
49811
49910
  /**
49812
49911
  * Local Scrypted YOLO catalog (Task 7B). Listing is a projection of the
49813
49912
  * revision-pinned manifesto plus a read-only HF drift check. Import accepts
49814
- * ONLY `manifestId` — host/repo/revision/path/labels/checksum stay server-owned.
49913
+ * `manifestId` (+ optional land-on-node) — host/repo/revision/path/labels/
49914
+ * checksum stay server-owned.
49815
49915
  */
49816
49916
  listScryptedCatalog: customAction(_void(), object({
49817
49917
  entries: array(ScryptedManifestEntrySchema).readonly(),
49818
49918
  drift: array(ScryptedCatalogDriftSchema).readonly()
49819
49919
  })),
49820
- importScryptedCatalogModel: customAction(object({ manifestId: string() }), CustomModelDescriptorSchema, { kind: "mutation" }),
49920
+ importScryptedCatalogModel: customAction(object({
49921
+ manifestId: string(),
49922
+ targetNodeId: string().optional()
49923
+ }), CustomModelDescriptorSchema, { kind: "mutation" }),
49821
49924
  /**
49822
49925
  * Frigate+ personal models. The API key lives in the addon's SETTINGS
49823
49926
  * (password field) and never leaves the hub: list / test / import all run
@@ -49827,9 +49930,9 @@ var modelStudioActions = defineCustomActions({
49827
49930
  /**
49828
49931
  * Nodes whose engine can actually LOAD a tflite artifact (the pool's only
49829
49932
  * 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.
49933
+ * Empty object — `addons.custom` cannot carry zod `void`.
49831
49934
  */
49832
- tfliteSupport: customAction(_void(), object({ nodes: array(string()).readonly() })),
49935
+ tfliteSupport: customAction(object({}).optional(), object({ nodes: array(string()).readonly() })),
49833
49936
  frigatePlusStatus: customAction(_void(), object({
49834
49937
  configured: boolean(),
49835
49938
  keyFormatValid: boolean()
@@ -49841,8 +49944,21 @@ var modelStudioActions = defineCustomActions({
49841
49944
  frigatePlusListModels: customAction(_void(), array(FrigatePlusModelSchema).readonly()),
49842
49945
  importFrigatePlusModel: customAction(object({
49843
49946
  plusModelId: string(),
49844
- modelId: string(),
49845
- name: string().optional()
49947
+ /** Ignored for identity — registry id is always `frigate-plus-<plusModelId>`. */
49948
+ modelId: string().optional(),
49949
+ name: string().optional(),
49950
+ /**
49951
+ * Native artifact class from the list row (`plusDownloadTraits`). The
49952
+ * model-detail API does not always carry `supportedDetectors`, so the
49953
+ * browser passes what the catalog already knew (fixes ssd/yolo-generic
49954
+ * EdgeTPU rows that `plusTypeTraits` alone would call onnx).
49955
+ */
49956
+ format: _enum(["onnx", "tflite"]).optional(),
49957
+ /**
49958
+ * Engine formats to convert after the native artifact lands. Empty =
49959
+ * download + register only (per-format Import clicks OV/CoreML later).
49960
+ */
49961
+ ensureFormats: array(_enum(["openvino", "coreml"])).readonly().optional()
49846
49962
  }), object({
49847
49963
  ok: literal(true),
49848
49964
  modelId: string(),
@@ -49925,6 +50041,15 @@ function spawnRunConvert(jobPath, pythonPath, onProgress, _sessionId) {
49925
50041
  });
49926
50042
  proc.stderr.on("data", (chunk) => {
49927
50043
  stderrChunks.push(chunk);
50044
+ for (const line of chunk.toString().split("\n")) {
50045
+ const trimmed = line.trimEnd();
50046
+ if (trimmed.length === 0) continue;
50047
+ onProgress({
50048
+ phase: "log",
50049
+ log: trimmed,
50050
+ detail: trimmed
50051
+ });
50052
+ }
49928
50053
  });
49929
50054
  proc.on("close", (code) => {
49930
50055
  if (code !== 0) {
@@ -50014,6 +50139,7 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
50014
50139
  phase: p.phase,
50015
50140
  pct: p.pct,
50016
50141
  detail: p.detail,
50142
+ log: p.log,
50017
50143
  sessionId
50018
50144
  }
50019
50145
  });
@@ -50057,11 +50183,21 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
50057
50183
  return result;
50058
50184
  },
50059
50185
  abortModelUpload: async (input) => (await this.getUploadStore()).abort(input),
50060
- registerCustomModel: async (input) => this.registerCustomModel(input),
50186
+ registerCustomModel: async (input) => this.registerCustomModel(input, { ensureEngines: true }),
50187
+ ensureFormatsThenDistribute: async (input) => this.ensureFormatsThenDistribute_(input.modelId),
50188
+ ensureModelFormats: async (input) => this.ensureModelFormats_(input.modelId, input.formats),
50061
50189
  removeCustomModel: async (input) => this.removeCustomModel(input),
50062
- listCustomModels: async () => this.listCustomModels(),
50190
+ removeModelFromNode: async (input) => this.removeModelFromNode(input),
50191
+ deleteSeedFromNode: async (input) => this.deleteSeedFromNode(input),
50192
+ listCustomModels: async () => {
50193
+ await this.pruneOrphanAvailability_();
50194
+ return this.listCustomModels();
50195
+ },
50063
50196
  distributeModel: async (input) => this.distributeModel(input),
50064
- listAvailability: async () => this.availability_.get(),
50197
+ listAvailability: async () => {
50198
+ await this.pruneOrphanAvailability_();
50199
+ return this.availability_.get();
50200
+ },
50065
50201
  listFrigateCatalog: async () => FRIGATE_PUBLIC_CATALOG.map((c) => ({
50066
50202
  id: c.id,
50067
50203
  label: c.label,
@@ -50206,10 +50342,37 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
50206
50342
  async distributeModel(input) {
50207
50343
  const descriptor = (await this.state_.get()).find((m) => m.entry.id === input.modelId);
50208
50344
  if (!descriptor) throw new Error(`distributeModel: model "${input.modelId}" is not registered`);
50345
+ const formatEntry = descriptor.entry.formats[input.format];
50346
+ if (formatEntry === void 0) throw new Error(`distributeModel: model "${input.modelId}" has no ${input.format} format in the Library`);
50209
50347
  const api = this.ctx.api;
50210
50348
  if (!api) throw new Error("distributeModel: ctx.api unavailable");
50349
+ if (/^https?:\/\//i.test(formatEntry.url)) {
50350
+ const downloaded = await api.pipelineExecutor.downloadModel.mutate({
50351
+ modelId: input.modelId,
50352
+ format: input.format,
50353
+ addonId: descriptor.stepId
50354
+ }, nodePin(input.nodeId));
50355
+ const bytes = Math.round(downloaded.sizeMB * 1e6);
50356
+ await this.availability_.update((prev) => upsertAvailability(prev, input.modelId, input.nodeId, {
50357
+ format: input.format,
50358
+ sha256: "",
50359
+ bytes,
50360
+ at: Date.now()
50361
+ }));
50362
+ this.ctx.logger.info("Downloaded custom model on node", { meta: {
50363
+ modelId: input.modelId,
50364
+ nodeId: input.nodeId,
50365
+ format: input.format,
50366
+ bytes
50367
+ } });
50368
+ return {
50369
+ ok: true,
50370
+ sha256: "",
50371
+ bytes
50372
+ };
50373
+ }
50211
50374
  const res = await api.modelDistributor.distributeModel.mutate({
50212
- nodeId: input.nodeId,
50375
+ targetNodeId: input.nodeId,
50213
50376
  modelId: input.modelId,
50214
50377
  format: input.format,
50215
50378
  entry: descriptor.entry
@@ -50288,59 +50451,49 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
50288
50451
  }
50289
50452
  }
50290
50453
  /**
50291
- * Import one manifesto entry by id. Host/repo/revision/path stay server-owned.
50454
+ * Register one manifesto entry by id into the Library. Host/repo/revision
50455
+ * stay server-owned. Artifacts are NOT staged on the hub — optional
50456
+ * `targetNodeId` lands the HF URL on that node via download-on-target.
50292
50457
  */
50293
50458
  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
- });
50459
+ const entry = getScryptedManifestEntry(input.manifestId);
50460
+ if (!entry) throw new Error(`importScryptedCatalogModel: unknown manifest id "${input.manifestId}"`);
50461
+ let drift;
50462
+ try {
50463
+ const tree = await fetchScryptedRemoteTree({ fetchImpl: fetch });
50464
+ const row = compareScryptedManifestDrift(SCRYPTED_YOLO_MANIFEST.filter((e) => e.id === input.manifestId), tree)[0];
50465
+ drift = normalizeScryptedDrift(row ?? {
50466
+ manifestId: input.manifestId,
50467
+ status: "missing",
50468
+ details: "entry missing from manifesto"
50469
+ });
50470
+ } catch (err) {
50471
+ drift = {
50472
+ manifestId: input.manifestId,
50473
+ status: "unavailable",
50474
+ details: err instanceof Error ? err.message : "Hugging Face tree unavailable"
50475
+ };
50476
+ }
50477
+ if (drift.status !== "ok") throw new Error(`importScryptedCatalogModel: entry "${entry.id}" is blocked by drift (${drift.status}${drift.details ? `: ${drift.details}` : ""})`);
50478
+ const descriptor = buildScryptedRegistryDescriptor(entry);
50321
50479
  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: {
50480
+ if (input.targetNodeId !== void 0 && input.targetNodeId.length > 0) await this.distributeModel({
50481
+ modelId: descriptor.entry.id,
50482
+ nodeId: input.targetNodeId,
50483
+ format: entry.format
50484
+ });
50485
+ this.ctx.logger.info("Registered Scrypted catalog model", { meta: {
50333
50486
  manifestId: input.manifestId,
50334
50487
  modelId: descriptor.entry.id,
50335
- bytes
50488
+ format: entry.format,
50489
+ ...input.targetNodeId !== void 0 ? { targetNodeId: input.targetNodeId } : {}
50336
50490
  } });
50337
50491
  return descriptor;
50338
50492
  }
50339
50493
  /**
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.
50494
+ * Register a curated PUBLIC Frigate catalog model into the Library. The
50495
+ * entry keeps its public URL — no hub staging. Optional `targetNodeId`
50496
+ * downloads on that node (same path as Library → Distribute).
50344
50497
  */
50345
50498
  async importFrigateCatalogModel(input) {
50346
50499
  const cat = FRIGATE_PUBLIC_CATALOG.find((c) => c.id === input.catalogId);
@@ -50349,29 +50502,21 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
50349
50502
  ...input.modelId !== void 0 ? { modelId: input.modelId } : {},
50350
50503
  ...input.name !== void 0 ? { name: input.name } : {}
50351
50504
  });
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
50505
  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: {
50506
+ let bytes = Math.round(cat.sizeMB * 1e6);
50507
+ if (input.targetNodeId !== void 0 && input.targetNodeId.length > 0) {
50508
+ const landed = await this.distributeModel({
50509
+ modelId: descriptor.entry.id,
50510
+ nodeId: input.targetNodeId,
50511
+ format: cat.format
50512
+ });
50513
+ bytes = landed.bytes > 0 ? landed.bytes : bytes;
50514
+ }
50515
+ this.ctx.logger.info("Registered Frigate catalog model", { meta: {
50371
50516
  catalogId: input.catalogId,
50372
50517
  modelId: descriptor.entry.id,
50373
- bytes,
50374
- sha256
50518
+ ...input.targetNodeId !== void 0 ? { targetNodeId: input.targetNodeId } : {},
50519
+ bytes
50375
50520
  } });
50376
50521
  return {
50377
50522
  ok: true,
@@ -50387,11 +50532,20 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
50387
50532
  * is hub-local: other nodes get it via Models → Distribute.
50388
50533
  */
50389
50534
  async importFrigatePlusModel(input) {
50390
- if (!/^[a-zA-Z0-9._-]+$/.test(input.modelId)) throw new Error(`importFrigatePlusModel: model id "${input.modelId}" is invalid`);
50535
+ const modelId = frigatePlusRegistryId(input.plusModelId);
50391
50536
  const key = this.config.frigatePlusApiKey ?? "";
50392
50537
  const { info, downloadUrl } = await requestPlusModelDownload(plusFetch, key, input.plusModelId);
50393
- const traits = plusTypeTraits(info.type);
50394
- const filename = `${input.modelId}${traits.extension}`;
50538
+ const typeTraits = plusTypeTraits(info.type);
50539
+ const traits = input.format === "tflite" ? {
50540
+ format: "tflite",
50541
+ extension: ".tflite",
50542
+ ...info.type === "ssd" || info.type === "mobiledet" ? { postprocessor: "ssd" } : {}
50543
+ } : input.format === "onnx" ? {
50544
+ format: "onnx",
50545
+ extension: ".onnx",
50546
+ ...typeTraits.postprocessor !== void 0 ? { postprocessor: typeTraits.postprocessor } : {}
50547
+ } : typeTraits;
50548
+ const filename = `${modelId}${traits.extension}`;
50395
50549
  const modelsDir = await this.resolveModelsDir();
50396
50550
  const destPath = path.join(modelsDir, filename);
50397
50551
  await downloadFile(downloadUrl, destPath);
@@ -50402,13 +50556,14 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
50402
50556
  }
50403
50557
  const sha256 = await ModelStudioAddon.fileSha256(destPath);
50404
50558
  const descriptor = plusInfoToDescriptor(info, {
50405
- modelId: input.modelId,
50559
+ modelId,
50406
50560
  name: input.name?.trim() || `Frigate+ ${info.type} (${input.plusModelId.slice(0, 8)})`,
50407
50561
  filename,
50408
- sizeMB: Number((bytes / 1e6).toFixed(1))
50562
+ sizeMB: Number((bytes / 1e6).toFixed(1)),
50563
+ ...input.format !== void 0 ? { format: input.format } : {}
50409
50564
  });
50410
- await this.registerCustomModel(descriptor);
50411
- await this.availability_.update((prev) => upsertAvailability(prev, input.modelId, "hub", {
50565
+ await this.registerCustomModel(descriptor, { ensureEngines: false });
50566
+ await this.availability_.update((prev) => upsertAvailability(prev, modelId, "hub", {
50412
50567
  format: traits.format,
50413
50568
  sha256,
50414
50569
  bytes,
@@ -50416,59 +50571,28 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
50416
50571
  }));
50417
50572
  this.ctx.logger.info("Imported Frigate+ model", { meta: {
50418
50573
  plusModelId: input.plusModelId,
50419
- modelId: input.modelId,
50574
+ modelId,
50420
50575
  type: info.type,
50421
- bytes
50576
+ bytes,
50577
+ format: traits.format
50422
50578
  } });
50579
+ const wanted = input.ensureFormats ?? [];
50423
50580
  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
- }
50581
+ if (traits.format === "onnx" && wanted.length > 0) {
50582
+ conversions = (await ensureEngineFormats(descriptor.entry, this.plusConversionDeps(async () => (await requestPlusModelDownload(plusFetch, key, input.plusModelId)).downloadUrl, descriptor.stepId), { targetFormats: wanted })).conversions;
50583
+ this.ctx.logger.info("Frigate+ import — engine conversions", { meta: {
50584
+ modelId,
50585
+ outcomes: conversions.map((o) => ({ ...o }))
50586
+ } });
50463
50587
  }
50464
50588
  return {
50465
50589
  ok: true,
50466
- modelId: input.modelId,
50590
+ modelId,
50467
50591
  bytes,
50468
50592
  conversions
50469
50593
  };
50470
50594
  }
50471
- async registerCustomModel(input) {
50595
+ async registerCustomModel(input, opts = {}) {
50472
50596
  if (!input.stepId.trim()) throw new Error("registerCustomModel: stepId is required");
50473
50597
  if (!input.entry.id.trim()) throw new Error("registerCustomModel: entry.id is required");
50474
50598
  await this.state_.update((prev) => upsertCustomModel(prev, input));
@@ -50476,9 +50600,252 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
50476
50600
  modelId: input.entry.id,
50477
50601
  stepId: input.stepId
50478
50602
  } });
50479
- return { ok: true };
50603
+ let conversions = [];
50604
+ if (opts.ensureEngines === true && input.entry.formats.onnx !== void 0) {
50605
+ const onnxUrl = input.entry.formats.onnx.url;
50606
+ if (typeof onnxUrl === "string" && onnxUrl.length > 0) {
50607
+ const deps = this.plusConversionDeps(() => Promise.resolve(onnxUrl), input.stepId);
50608
+ conversions = (await ensureEngineFormats(input.entry, deps)).conversions;
50609
+ if (conversions.length > 0) this.ctx.logger.info("Custom model — engine conversions", { meta: {
50610
+ modelId: input.entry.id,
50611
+ outcomes: conversions.map((o) => ({ ...o }))
50612
+ } });
50613
+ }
50614
+ }
50615
+ return {
50616
+ ok: true,
50617
+ conversions
50618
+ };
50619
+ }
50620
+ plusConversionDeps(freshSourceUrl, stepId) {
50621
+ const api = this.ctx.api;
50622
+ return {
50623
+ recommend: (i) => recommendConvertNode(i, this.recommendDeps()),
50624
+ convert: async (convertInput, nId) => {
50625
+ if (!api) throw new Error("modelConvert unavailable");
50626
+ return api.modelConvert.convert.mutate(convertInput, nodePin(nId));
50627
+ },
50628
+ freshSourceUrl,
50629
+ upsertEntry: async (entry) => {
50630
+ await this.state_.update((prev) => upsertCustomModel(prev, {
50631
+ stepId,
50632
+ entry
50633
+ }));
50634
+ },
50635
+ markAvailable: async (nId, mId, artifacts) => {
50636
+ await this.availability_.update((prev) => {
50637
+ const seen = /* @__PURE__ */ new Set();
50638
+ let next = prev;
50639
+ for (const artifact of artifacts) {
50640
+ if (seen.has(artifact.format)) continue;
50641
+ seen.add(artifact.format);
50642
+ next = upsertAvailability(next, mId, nId, {
50643
+ format: artifact.format,
50644
+ sha256: "",
50645
+ bytes: Math.round(artifact.sizeMB * 1e6),
50646
+ at: Date.now()
50647
+ });
50648
+ }
50649
+ return next;
50650
+ });
50651
+ }
50652
+ };
50653
+ }
50654
+ async ensureModelFormats_(modelId, formats) {
50655
+ const row = (await this.state_.get()).find((m) => m.entry.id === modelId);
50656
+ if (row === void 0) throw new Error(`ensureModelFormats: model "${modelId}" is not registered`);
50657
+ const onnxUrl = row.entry.formats.onnx?.url;
50658
+ if (typeof onnxUrl !== "string" || onnxUrl.length === 0) throw new Error(`ensureModelFormats: model "${modelId}" has no ONNX to convert from`);
50659
+ const ensured = await ensureEngineFormats(row.entry, this.plusConversionDeps(async () => {
50660
+ const url = ((await this.state_.get()).find((m) => m.entry.id === modelId)?.entry)?.formats.onnx?.url ?? onnxUrl;
50661
+ if (typeof url !== "string" || url.length === 0) throw new Error(`ensureModelFormats: no ONNX url for "${modelId}"`);
50662
+ return url;
50663
+ }, row.stepId), { targetFormats: formats });
50664
+ this.ctx.logger.info("ensureModelFormats", { meta: {
50665
+ modelId,
50666
+ outcomes: ensured.conversions.map((o) => ({ ...o }))
50667
+ } });
50668
+ return { conversions: ensured.conversions };
50480
50669
  }
50670
+ async ensureFormatsThenDistribute_(modelId) {
50671
+ const api = this.ctx.api;
50672
+ if (!api) throw new Error("ensureFormatsThenDistribute: ctx.api unavailable");
50673
+ const getEntry = async () => {
50674
+ return (await this.state_.get()).find((m) => m.entry.id === modelId)?.entry;
50675
+ };
50676
+ const entry = await getEntry();
50677
+ if (entry === void 0) throw new Error(`ensureFormatsThenDistribute: model "${modelId}" is not registered`);
50678
+ const onnxUrl = entry.formats.onnx?.url;
50679
+ const stepId = (await this.state_.get()).find((m) => m.entry.id === modelId)?.stepId ?? "object-detection";
50680
+ return ensureFormatsThenDistribute(modelId, {
50681
+ ...this.plusConversionDeps(async () => {
50682
+ const url = (await getEntry())?.formats.onnx?.url ?? onnxUrl;
50683
+ if (typeof url !== "string" || url.length === 0) throw new Error(`ensureFormatsThenDistribute: no ONNX url for "${modelId}"`);
50684
+ return url;
50685
+ }, stepId),
50686
+ getEntry,
50687
+ listAgentNodeIds: async () => {
50688
+ return (await api.nodes.directory.query()).nodes.filter((n) => n.isOnline && !n.isHub).map((n) => n.id);
50689
+ },
50690
+ distribute: async (mId, nodeId, format) => {
50691
+ const res = await this.distributeModel({
50692
+ modelId: mId,
50693
+ nodeId,
50694
+ format
50695
+ });
50696
+ return {
50697
+ sha256: res.sha256,
50698
+ bytes: res.bytes
50699
+ };
50700
+ }
50701
+ });
50702
+ }
50703
+ /**
50704
+ * Delete one CamStack seed format on one node. Physically removes files from
50705
+ * that node's modelsDir via `pipelineExecutor.deleteModel` + `nodePin`.
50706
+ */
50707
+ async deleteSeedFromNode(input) {
50708
+ const api = this.ctx.api;
50709
+ if (!api) throw new Error("deleteSeedFromNode: API not available");
50710
+ await api.pipelineExecutor.deleteModel.mutate({
50711
+ addonId: input.addonId,
50712
+ modelId: input.modelId,
50713
+ format: input.format
50714
+ }, nodePin(input.nodeId));
50715
+ this.ctx.logger.info("CamStack seed deleted from node disk", { meta: {
50716
+ modelId: input.modelId,
50717
+ format: input.format,
50718
+ nodeId: input.nodeId,
50719
+ addonId: input.addonId
50720
+ } });
50721
+ return { success: true };
50722
+ }
50723
+ /**
50724
+ * Delete one format on one node. Registry row stays; only that availability
50725
+ * chip is cleared. Pins are left alone — re-Send to node restores files.
50726
+ */
50727
+ async removeModelFromNode(input) {
50728
+ const descriptor = (await this.state_.get()).find((m) => m.entry.id === input.modelId);
50729
+ if (!descriptor) throw new Error(`removeModelFromNode: model "${input.modelId}" is not registered`);
50730
+ if (descriptor.entry.formats[input.format] === void 0) throw new Error(`removeModelFromNode: model "${input.modelId}" has no ${input.format} format in the Library`);
50731
+ let deleted = false;
50732
+ const api = this.ctx.api;
50733
+ try {
50734
+ if (api) {
50735
+ await api.pipelineExecutor.deleteModel.mutate({
50736
+ addonId: descriptor.stepId,
50737
+ modelId: input.modelId,
50738
+ format: input.format
50739
+ }, nodePin(input.nodeId));
50740
+ deleted = true;
50741
+ }
50742
+ } catch (err) {
50743
+ this.ctx.logger.warn("removeModelFromNode: remote delete failed (continuing)", { meta: {
50744
+ modelId: input.modelId,
50745
+ nodeId: input.nodeId,
50746
+ format: input.format,
50747
+ error: err instanceof Error ? err.message : String(err)
50748
+ } });
50749
+ }
50750
+ if (input.nodeId === "hub") try {
50751
+ deleteModelFromDisk(await this.resolveModelsDir(), descriptor.entry, input.format);
50752
+ deleted = true;
50753
+ } catch (err) {
50754
+ this.ctx.logger.warn("removeModelFromNode: hub disk wipe failed", { meta: {
50755
+ modelId: input.modelId,
50756
+ format: input.format,
50757
+ error: err instanceof Error ? err.message : String(err)
50758
+ } });
50759
+ }
50760
+ let clearedAvailability = false;
50761
+ await this.availability_.update((prev) => {
50762
+ const result = clearAvailabilityFormat(prev, input.modelId, input.nodeId, input.format);
50763
+ clearedAvailability = result.cleared;
50764
+ return result.next;
50765
+ });
50766
+ this.ctx.logger.info("Removed custom model format from node", { meta: {
50767
+ modelId: input.modelId,
50768
+ nodeId: input.nodeId,
50769
+ format: input.format,
50770
+ deleted,
50771
+ clearedAvailability
50772
+ } });
50773
+ return {
50774
+ deleted,
50775
+ clearedAvailability
50776
+ };
50777
+ }
50778
+ /**
50779
+ * Full purge of a Library model: delete artifacts on every node that had
50780
+ * them, clear availability (so Sources no longer shows "imported"), strip
50781
+ * pipeline pins that named this modelId (agents revert to hardware defaults
50782
+ * for that step), then drop the registry row.
50783
+ */
50481
50784
  async removeCustomModel(input) {
50785
+ const descriptor = (await this.state_.get()).find((m) => m.entry.id === input.modelId);
50786
+ if (!descriptor) {
50787
+ await this.availability_.update((prev) => clearAvailability(prev, input.modelId).next);
50788
+ return {
50789
+ removed: false,
50790
+ deletedNodes: [],
50791
+ clearedPins: 0
50792
+ };
50793
+ }
50794
+ const formats = Object.keys(descriptor.entry.formats).filter((f) => descriptor.entry.formats[f] !== void 0);
50795
+ const avail = await this.availability_.get();
50796
+ const nodeIds = new Set(["hub", ...Object.keys(avail[input.modelId] ?? {})]);
50797
+ const deletedNodes = [];
50798
+ const api = this.ctx.api;
50799
+ for (const nodeId of nodeIds) {
50800
+ let anyDeleted = false;
50801
+ for (const format of formats) try {
50802
+ if (api) {
50803
+ await api.pipelineExecutor.deleteModel.mutate({
50804
+ addonId: descriptor.stepId,
50805
+ modelId: input.modelId,
50806
+ format
50807
+ }, nodePin(nodeId));
50808
+ anyDeleted = true;
50809
+ }
50810
+ } catch (err) {
50811
+ this.ctx.logger.warn("removeCustomModel: remote delete failed (continuing)", { meta: {
50812
+ modelId: input.modelId,
50813
+ nodeId,
50814
+ format,
50815
+ error: err instanceof Error ? err.message : String(err)
50816
+ } });
50817
+ }
50818
+ if (anyDeleted) deletedNodes.push(nodeId);
50819
+ }
50820
+ try {
50821
+ const modelsDir = await this.resolveModelsDir();
50822
+ for (const format of formats) deleteModelFromDisk(modelsDir, descriptor.entry, format);
50823
+ } catch (err) {
50824
+ this.ctx.logger.warn("removeCustomModel: hub disk wipe failed", { meta: {
50825
+ modelId: input.modelId,
50826
+ error: err instanceof Error ? err.message : String(err)
50827
+ } });
50828
+ }
50829
+ let clearedPins = 0;
50830
+ if (api) {
50831
+ try {
50832
+ clearedPins += await this.clearAgentModelPins(input.modelId);
50833
+ } catch (err) {
50834
+ this.ctx.logger.warn("removeCustomModel: agent pin clear failed", { meta: {
50835
+ modelId: input.modelId,
50836
+ error: err instanceof Error ? err.message : String(err)
50837
+ } });
50838
+ }
50839
+ try {
50840
+ clearedPins += await this.clearCameraModelPins(input.modelId, descriptor.stepId);
50841
+ } catch (err) {
50842
+ this.ctx.logger.warn("removeCustomModel: camera pin clear failed", { meta: {
50843
+ modelId: input.modelId,
50844
+ error: err instanceof Error ? err.message : String(err)
50845
+ } });
50846
+ }
50847
+ }
50848
+ await this.availability_.update((prev) => clearAvailability(prev, input.modelId).next);
50482
50849
  let removed = false;
50483
50850
  await this.state_.update((prev) => {
50484
50851
  const result = removeCustomModel(prev, input.modelId);
@@ -50487,13 +50854,80 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
50487
50854
  });
50488
50855
  this.ctx.logger.info("Removed custom model", { meta: {
50489
50856
  modelId: input.modelId,
50490
- removed
50857
+ removed,
50858
+ deletedNodes,
50859
+ clearedPins
50491
50860
  } });
50492
- return { removed };
50861
+ return {
50862
+ removed,
50863
+ deletedNodes,
50864
+ clearedPins
50865
+ };
50866
+ }
50867
+ /** Strip `modelId` from every agent's inferenceDevices step pins. */
50868
+ async clearAgentModelPins(modelId) {
50869
+ const api = this.ctx.api;
50870
+ if (!api) return 0;
50871
+ const agents = await api.pipelineOrchestrator.listAgentSettings.query();
50872
+ let cleared = 0;
50873
+ for (const agent of agents) {
50874
+ const devices = agent.settings.inferenceDevices;
50875
+ if (devices === void 0) continue;
50876
+ const { next, cleared: n } = stripModelIdFromInferenceDevices(devices, modelId);
50877
+ if (n === 0) continue;
50878
+ await api.pipelineOrchestrator.setAgentInferenceDevices.mutate({
50879
+ agentNodeId: agent.nodeId,
50880
+ inferenceDevices: next
50881
+ });
50882
+ cleared += n;
50883
+ }
50884
+ return cleared;
50885
+ }
50886
+ /**
50887
+ * Clear per-camera step overrides that pin `modelId` for `stepId`.
50888
+ * `patch: null` drops the override so the camera inherits the agent default.
50889
+ */
50890
+ async clearCameraModelPins(modelId, stepId) {
50891
+ const api = this.ctx.api;
50892
+ if (!api) return 0;
50893
+ const devices = await api.deviceManager.listAll.query({ projection: "slim" });
50894
+ let cleared = 0;
50895
+ for (const device of devices) {
50896
+ const overrides = await api.pipelineOrchestrator.getCameraStepOverrides.query({ deviceId: device.id });
50897
+ if (overrides === null) continue;
50898
+ for (const [agentNodeId, byDeviceKey] of Object.entries(overrides)) for (const [deviceKey, byStep] of Object.entries(byDeviceKey)) {
50899
+ if (byStep[stepId]?.modelId !== modelId) continue;
50900
+ await api.pipelineOrchestrator.setCameraStepOverride.mutate({
50901
+ deviceId: device.id,
50902
+ agentNodeId,
50903
+ deviceKey,
50904
+ addonId: stepId,
50905
+ patch: null
50906
+ });
50907
+ cleared += 1;
50908
+ }
50909
+ }
50910
+ return cleared;
50493
50911
  }
50494
50912
  async listCustomModels() {
50495
50913
  return this.state_.get();
50496
50914
  }
50915
+ /**
50916
+ * Self-heal: drop availability rows for modelIds no longer in the Library
50917
+ * registry. Import and Library both trigger this so an orphan cannot keep
50918
+ * Sources stuck on "imported" after a registry wipe.
50919
+ */
50920
+ async pruneOrphanAvailability_() {
50921
+ try {
50922
+ const [avail, models] = await Promise.all([this.availability_.get(), this.state_.get()]);
50923
+ const { next, pruned } = pruneOrphanAvailability(avail, new Set(models.map((m) => m.entry.id)));
50924
+ if (pruned.length === 0) return;
50925
+ await this.availability_.update(() => next);
50926
+ this.ctx.logger.info("Pruned orphan model availability", { meta: { pruned } });
50927
+ } catch (err) {
50928
+ this.ctx.logger.warn("pruneOrphanAvailability failed (continuing)", { meta: { error: err instanceof Error ? err.message : String(err) } });
50929
+ }
50930
+ }
50497
50931
  };
50498
50932
  //#endregion
50499
50933
  export { ModelStudioAddon, modelStudioActions as t };