@camstack/addon-model-studio 1.1.34 → 1.1.37

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 (19) hide show
  1. package/dist/{MotionZonesSettings-W4fQr3y3.mjs → MotionZonesSettings-D1fhnzel.mjs} +2 -2
  2. package/dist/{PrivacyMaskSettings-CtKzQjYD.mjs → PrivacyMaskSettings-1RaBK3Qz.mjs} +4 -4
  3. package/dist/{SceneMonitorEditor-CM3z_N4Q.mjs → SceneMonitorEditor-CBciXVys.mjs} +3 -3
  4. package/dist/_stub.js +3266 -2335
  5. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-BGvyoZxl.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-DOKJR0Ti.mjs} +4 -4
  6. package/dist/_virtual_mf___mfe_internal__addon_model_studio_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Cjty59ip.mjs +26 -0
  7. package/dist/addon-model-studio.css +1 -1
  8. package/dist/{hostInit-BKRnEFn7.mjs → hostInit-CXMR7hTR.mjs} +3 -3
  9. package/dist/model-studio.addon.js +934 -40
  10. package/dist/model-studio.addon.mjs +934 -40
  11. package/dist/{player-overlays-CNkUzVZI.mjs → player-overlays-CugH3jmy.mjs} +1 -1
  12. package/dist/remoteEntry.js +1 -1
  13. package/dist/{responsive-D-JIMJTX.mjs → responsive-BDnwOe1x.mjs} +1 -1
  14. package/dist/{square-CBwtzgNH.mjs → square-CcFXMJ6r.mjs} +1 -1
  15. package/dist/{trash-2-CiJaurwE.mjs → trash-2-D-t4fB0d.mjs} +1 -1
  16. package/dist/{use-device-snapshot-DpcJhtJX.mjs → use-device-snapshot-Cv0pmt1D.mjs} +1 -1
  17. package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-BqGMvm3d.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-CrZuQUCz.mjs} +1 -1
  18. package/package.json +1 -1
  19. package/dist/_virtual_mf___mfe_internal__addon_model_studio_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Boi8PUUw.mjs +0 -26
@@ -34,7 +34,7 @@ let node_path$1 = __toESM(node_path, 1);
34
34
  node_path = __toESM(node_path);
35
35
  let node_util = require("node:util");
36
36
  let node_zlib = require("node:zlib");
37
- //#region ../system/dist/file-data-plane-CuE_hBli.mjs
37
+ //#region ../system/dist/file-data-plane-BhKdxJgf.mjs
38
38
  /** Build fetch headers, including HF auth token for huggingface.co URLs */
39
39
  function buildHeaders(url) {
40
40
  const headers = { "User-Agent": "CamStack/1.0" };
@@ -42,21 +42,56 @@ function buildHeaders(url) {
42
42
  if (hfToken && url.includes("huggingface.co")) headers["Authorization"] = `Bearer ${hfToken}`;
43
43
  return headers;
44
44
  }
45
- /**
46
- * Download a single file from a URL to a destination path.
47
- * Uses native fetch() (Node 22+) which handles redirects natively.
48
- * Streams to disk with optional progress callback.
49
- * Returns the destination path. Skips download if file already exists.
50
- */
51
- async function downloadFile(url, destPath, onProgress) {
45
+ var DEFAULT_MAX_REDIRECTS = 5;
46
+ function normalizeDownloadOptions(third) {
47
+ if (typeof third === "function") return { onProgress: third };
48
+ return third ?? {};
49
+ }
50
+ function isRedirectStatus(status) {
51
+ return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
52
+ }
53
+ function resolveRedirectUrl(current, location) {
54
+ return new URL(location, current);
55
+ }
56
+ async function downloadFile(url, destPath, onProgressOrOptions) {
52
57
  if (node_fs$1.existsSync(destPath)) return destPath;
58
+ const opts = normalizeDownloadOptions(onProgressOrOptions);
59
+ const fetchImpl = opts.fetchImpl ?? fetch;
60
+ const maxRedirects = opts.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
53
61
  node_fs$1.mkdirSync(node_path$1.dirname(destPath), { recursive: true });
54
62
  const tmpPath = destPath + ".downloading";
55
63
  try {
56
- const response = await fetch(url, {
57
- redirect: "follow",
58
- headers: buildHeaders(url)
59
- });
64
+ let current = url;
65
+ const seen = /* @__PURE__ */ new Set();
66
+ let response;
67
+ const manual = opts.redirectPolicy !== void 0;
68
+ for (let hop = 0; hop <= maxRedirects; hop++) {
69
+ const parsed = new URL(current);
70
+ if (opts.redirectPolicy) opts.redirectPolicy(parsed, hop);
71
+ if (seen.has(parsed.href)) throw new Error(`Redirect loop detected at ${parsed.href}`);
72
+ seen.add(parsed.href);
73
+ const controller = opts.timeoutMs !== void 0 ? new AbortController() : void 0;
74
+ const timer = controller && opts.timeoutMs !== void 0 ? setTimeout(() => controller.abort(), opts.timeoutMs) : void 0;
75
+ try {
76
+ response = await fetchImpl(current, {
77
+ redirect: manual ? "manual" : "follow",
78
+ headers: buildHeaders(current),
79
+ ...controller ? { signal: controller.signal } : {}
80
+ });
81
+ } finally {
82
+ if (timer) clearTimeout(timer);
83
+ }
84
+ if (manual && isRedirectStatus(response.status)) {
85
+ const location = response.headers.get("location");
86
+ if (!location) throw new Error(`Redirect ${response.status} missing Location header`);
87
+ if (hop >= maxRedirects) throw new Error(`Too many redirects (max ${maxRedirects}) downloading ${url}`);
88
+ current = resolveRedirectUrl(current, location).href;
89
+ continue;
90
+ }
91
+ break;
92
+ }
93
+ if (!response) throw new Error(`No response downloading ${url}`);
94
+ if (manual && isRedirectStatus(response.status)) throw new Error(`Too many redirects (max ${maxRedirects}) downloading ${url}`);
60
95
  if (!response.ok) throw new Error(`HTTP ${response.status} downloading ${url}`);
61
96
  if (!response.body) throw new Error(`No response body from ${url}`);
62
97
  const total = parseInt(response.headers.get("content-length") ?? "0", 10);
@@ -67,9 +102,10 @@ async function downloadFile(url, destPath, onProgress) {
67
102
  for (;;) {
68
103
  const { done, value } = await reader.read();
69
104
  if (done || !value) break;
70
- fileStream.write(value);
71
105
  downloaded += value.length;
72
- onProgress?.(downloaded, total);
106
+ if (opts.maxBytes !== void 0 && downloaded > opts.maxBytes) throw new Error(`Downloaded ${downloaded} bytes exceeds the ${opts.maxBytes}-byte limit`);
107
+ fileStream.write(value);
108
+ opts.onProgress?.(downloaded, total);
73
109
  }
74
110
  } finally {
75
111
  fileStream.end();
@@ -90,6 +126,495 @@ async function downloadFile(url, destPath, onProgress) {
90
126
  (0, node_util.promisify)(node_zlib.brotliCompress);
91
127
  (0, node_util.promisify)(node_zlib.gzip);
92
128
  //#endregion
129
+ //#region src/scrypted/scrypted-download-policy.ts
130
+ /**
131
+ * Strict per-hop allow-list for Scrypted catalog downloads.
132
+ *
133
+ * Hosts are the ones observed from the pinned revision
134
+ * `187402b20a3be0a7fab9bceee21d857ed6ae0064` — not a generic CDN wildcard.
135
+ */
136
+ var SCRYPTED_DOWNLOAD_REDIRECT_HOSTS = ["huggingface.co", "us.aws.cdn.hf.co"];
137
+ var LOCAL_HOSTS = new Set([
138
+ "localhost",
139
+ "127.0.0.1",
140
+ "::1",
141
+ "0.0.0.0"
142
+ ]);
143
+ var IPV4_RE = /^(?:\d{1,3}\.){3}\d{1,3}$/;
144
+ function assertScryptedDownloadUrl(url, _hop) {
145
+ if (url.protocol !== "https:") throw new Error(`http downgrade refused: ${url.protocol}`);
146
+ if (url.username || url.password) throw new Error("userinfo not allowed on download URL");
147
+ const host = url.hostname.toLowerCase();
148
+ if (LOCAL_HOSTS.has(host) || IPV4_RE.test(host) || host.includes(":")) throw new Error(`host not allowed: ${host}`);
149
+ if (!SCRYPTED_DOWNLOAD_REDIRECT_HOSTS.includes(host)) throw new Error(`host not allowed: ${host}`);
150
+ }
151
+ //#endregion
152
+ //#region src/scrypted/scrypted-yolo-manifest-data.json
153
+ 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}]");
154
+ //#endregion
155
+ //#region src/scrypted/scrypted-yolo-manifest.ts
156
+ var SCRYPTED_SHA256_RE = /^[0-9a-f]{64}$/;
157
+ var SCRYPTED_HF_HOST = "https://huggingface.co";
158
+ var SCRYPTED_HF_REPO = "scrypted/plugin-models";
159
+ var SCRYPTED_HF_REVISION = "187402b20a3be0a7fab9bceee21d857ed6ae0064";
160
+ var SCRYPTED_YOLO_LABELS = [
161
+ "person",
162
+ "vehicle",
163
+ "animal"
164
+ ];
165
+ var SCRYPTED_IDENTITY_CLASS_MAP = {
166
+ mapping: {
167
+ person: "person",
168
+ vehicle: "vehicle",
169
+ animal: "animal"
170
+ },
171
+ preserveOriginal: false
172
+ };
173
+ var SCRYPTED_LABEL_DEFS = SCRYPTED_YOLO_LABELS.map((id) => ({
174
+ id,
175
+ name: id
176
+ }));
177
+ var MLPACKAGE_INNER_FILES = [
178
+ "Manifest.json",
179
+ "Data/com.apple.CoreML/model.mlmodel",
180
+ "Data/com.apple.CoreML/weights/weight.bin"
181
+ ];
182
+ var INPUT_SIZE = {
183
+ width: 320,
184
+ height: 320
185
+ };
186
+ function assertScryptedFileSha256(value, relativePath) {
187
+ if (value === void 0 || !SCRYPTED_SHA256_RE.test(value)) throw new Error(`Invalid sha256 for ${relativePath}: expected 64 lowercase hex`);
188
+ return value;
189
+ }
190
+ function assertScryptedRelativePath(value) {
191
+ if (value.length === 0 || value.startsWith("/") || value.includes("\\") || value.includes("\0")) throw new Error(`Unsafe Scrypted relative path: ${value}`);
192
+ if (value.split("/").some((segment) => segment.length === 0 || segment === "." || segment === "..")) throw new Error(`Unsafe Scrypted relative path: ${value}`);
193
+ return value;
194
+ }
195
+ function buildScryptedResolveUrl(relativePath) {
196
+ return `${SCRYPTED_HF_HOST}/${SCRYPTED_HF_REPO}/resolve/${SCRYPTED_HF_REVISION}/${assertScryptedRelativePath(relativePath).split("/").map((segment) => encodeURIComponent(segment)).join("/")}`;
197
+ }
198
+ function packageDirectory(files) {
199
+ const manifest = files.find((file) => file.relativePath.endsWith("/Manifest.json"));
200
+ if (!manifest) throw new Error("CoreML entry is missing Manifest.json");
201
+ return assertScryptedRelativePath(manifest.relativePath.replace(/\/Manifest\.json$/, ""));
202
+ }
203
+ function buildModel(spec) {
204
+ const group = {
205
+ family: "yolov9",
206
+ tier: spec.family.slice(-1),
207
+ resolution: 320,
208
+ ...spec.variant === "relu_test" ? { optimization: "fast" } : {},
209
+ ...spec.precision === "int8" ? { precision: "int8" } : {}
210
+ };
211
+ if (spec.format === "coreml") {
212
+ const dir = packageDirectory(spec.files);
213
+ return {
214
+ id: spec.id,
215
+ name: spec.label,
216
+ description: `${spec.label} — imported from the local Scrypted YOLO catalog.`,
217
+ formats: { coreml: {
218
+ url: buildScryptedResolveUrl(dir),
219
+ sizeMB: spec.sizeMB,
220
+ isDirectory: true,
221
+ files: [...MLPACKAGE_INNER_FILES],
222
+ runtimes: ["python"]
223
+ } },
224
+ inputSize: INPUT_SIZE,
225
+ labels: SCRYPTED_LABEL_DEFS,
226
+ preprocessMode: "resize",
227
+ classMap: SCRYPTED_IDENTITY_CLASS_MAP,
228
+ group
229
+ };
230
+ }
231
+ if (spec.format === "openvino") {
232
+ const xml = spec.files.find((file) => file.relativePath.endsWith(".xml"));
233
+ const bin = spec.files.find((file) => file.relativePath.endsWith(".bin"));
234
+ if (!xml || !bin) throw new Error(`OpenVINO entry ${spec.id} is missing xml/bin`);
235
+ const binName = bin.relativePath.split("/").pop() ?? "best-converted.bin";
236
+ return {
237
+ id: spec.id,
238
+ name: spec.label,
239
+ description: `${spec.label} — imported from the local Scrypted YOLO catalog.`,
240
+ formats: { openvino: {
241
+ url: buildScryptedResolveUrl(xml.relativePath),
242
+ sizeMB: spec.sizeMB,
243
+ files: [binName],
244
+ runtimes: ["python"]
245
+ } },
246
+ inputSize: INPUT_SIZE,
247
+ labels: SCRYPTED_LABEL_DEFS,
248
+ preprocessMode: "resize",
249
+ classMap: SCRYPTED_IDENTITY_CLASS_MAP,
250
+ group
251
+ };
252
+ }
253
+ const main = spec.files[0];
254
+ if (!main) throw new Error(`Entry ${spec.id} has no files`);
255
+ return {
256
+ id: spec.id,
257
+ name: spec.label,
258
+ description: `${spec.label} — imported from the local Scrypted YOLO catalog.`,
259
+ formats: { [spec.format]: {
260
+ url: buildScryptedResolveUrl(main.relativePath),
261
+ sizeMB: spec.sizeMB,
262
+ runtimes: spec.format === "tflite" ? ["python"] : void 0
263
+ } },
264
+ inputSize: INPUT_SIZE,
265
+ labels: SCRYPTED_LABEL_DEFS,
266
+ preprocessMode: "resize",
267
+ classMap: SCRYPTED_IDENTITY_CLASS_MAP,
268
+ group
269
+ };
270
+ }
271
+ function freezeEntry(spec) {
272
+ const files = spec.files.map((file) => Object.freeze({
273
+ relativePath: assertScryptedRelativePath(file.relativePath),
274
+ size: file.size,
275
+ sha256: assertScryptedFileSha256(file.sha256, file.relativePath)
276
+ }));
277
+ return Object.freeze({
278
+ id: spec.id,
279
+ label: spec.label,
280
+ family: spec.family,
281
+ variant: spec.variant,
282
+ format: spec.format,
283
+ precision: spec.precision,
284
+ inputSize: INPUT_SIZE,
285
+ files: Object.freeze(files),
286
+ model: buildModel({
287
+ ...spec,
288
+ files
289
+ })
290
+ });
291
+ }
292
+ var SCRYPTED_YOLO_MANIFEST = Object.freeze(scrypted_yolo_manifest_data_default.map(freezeEntry));
293
+ function getScryptedManifestEntry(manifestId) {
294
+ return SCRYPTED_YOLO_MANIFEST.find((entry) => entry.id === manifestId);
295
+ }
296
+ //#endregion
297
+ //#region src/scrypted/scrypted-tree-drift.ts
298
+ /**
299
+ * Read-only drift check: compare the local Scrypted YOLO manifest to a
300
+ * Hugging Face tree. Never mutates the manifest and never auto-imports
301
+ * remote-only files.
302
+ */
303
+ var SCRYPTED_KNOWN_DRIFT_STATUSES = [
304
+ "ok",
305
+ "missing",
306
+ "size-changed",
307
+ "unavailable"
308
+ ];
309
+ /** Fail-closed: a future/unknown status is never treated as `ok`. */
310
+ function normalizeScryptedDrift(row) {
311
+ if (SCRYPTED_KNOWN_DRIFT_STATUSES.includes(row.status)) return {
312
+ manifestId: row.manifestId,
313
+ status: row.status,
314
+ ...row.details !== void 0 ? { details: row.details } : {}
315
+ };
316
+ return {
317
+ manifestId: row.manifestId,
318
+ status: "unavailable",
319
+ details: row.details ?? `unknown drift status: ${row.status}`
320
+ };
321
+ }
322
+ function remoteSize(file) {
323
+ return file.lfs?.size ?? file.size;
324
+ }
325
+ function compareScryptedManifestDrift(manifest, tree) {
326
+ const byPath = new Map(tree.map((file) => [file.path, file]));
327
+ return manifest.map((entry) => {
328
+ const missing = [];
329
+ const changed = [];
330
+ for (const file of entry.files) {
331
+ const remote = byPath.get(file.relativePath);
332
+ if (!remote) {
333
+ missing.push(file.relativePath);
334
+ continue;
335
+ }
336
+ if (remoteSize(remote) !== file.size) changed.push(`${file.relativePath} declared ${file.size} remote ${remoteSize(remote)}`);
337
+ }
338
+ if (missing.length > 0) return {
339
+ manifestId: entry.id,
340
+ status: "missing",
341
+ details: `missing: ${missing.join(", ")}`
342
+ };
343
+ if (changed.length > 0) return {
344
+ manifestId: entry.id,
345
+ status: "size-changed",
346
+ details: `size-changed: ${changed.join(", ")}`
347
+ };
348
+ return {
349
+ manifestId: entry.id,
350
+ status: "ok"
351
+ };
352
+ });
353
+ }
354
+ var ScryptedTreeLinkError = class extends Error {
355
+ constructor(message) {
356
+ super(message);
357
+ this.name = "ScryptedTreeLinkError";
358
+ }
359
+ };
360
+ function assertScryptedTreePageUrl(url, revision) {
361
+ if (url.protocol !== "https:") throw new ScryptedTreeLinkError(`Refusing non-https tree link: ${url.protocol}`);
362
+ if (url.username || url.password) throw new ScryptedTreeLinkError("Refusing tree link with userinfo");
363
+ if (url.origin !== `https://huggingface.co`) throw new ScryptedTreeLinkError(`Refusing tree link origin ${url.origin}`);
364
+ const expectedPath = `/api/models/${SCRYPTED_HF_REPO}/tree/${revision}`;
365
+ if (url.pathname !== expectedPath) throw new ScryptedTreeLinkError(`Refusing tree link path/revision ${url.pathname} (expected ${expectedPath})`);
366
+ }
367
+ /**
368
+ * Parse `Link: <...>; rel="next"` and accept only an HTTPS huggingface.co
369
+ * tree URL for the same repo + revision. Relative hrefs resolve against the
370
+ * current page URL. Never returns a URL that failed validation.
371
+ */
372
+ function resolveScryptedTreeNextLink(header, currentUrl, revision) {
373
+ if (!header) return void 0;
374
+ const match = /<([^>]+)>;\s*rel="next"/.exec(header);
375
+ if (!match?.[1]) return void 0;
376
+ let resolved;
377
+ try {
378
+ resolved = new URL(match[1], currentUrl);
379
+ } catch {
380
+ throw new ScryptedTreeLinkError(`Refusing unparsable tree next link: ${match[1]}`);
381
+ }
382
+ assertScryptedTreePageUrl(resolved, revision);
383
+ return resolved.href;
384
+ }
385
+ async function sleep(ms) {
386
+ if (ms <= 0) return;
387
+ await new Promise((resolve) => setTimeout(resolve, ms));
388
+ }
389
+ async function fetchScryptedRemoteTree(opts) {
390
+ const revision = opts.revision ?? "main";
391
+ const maxRetries = opts.maxRetries ?? 4;
392
+ const retryDelayMs = opts.retryDelayMs ?? 200;
393
+ let lastError = /* @__PURE__ */ new Error("Scrypted Hugging Face tree unavailable");
394
+ for (let attempt = 0; attempt < maxRetries; attempt++) try {
395
+ const files = [];
396
+ let url = `${SCRYPTED_HF_HOST}/api/models/${SCRYPTED_HF_REPO}/tree/${encodeURIComponent(revision)}?recursive=true&expand=true`;
397
+ let pages = 0;
398
+ while (url) {
399
+ pages += 1;
400
+ if (pages > 50) throw new Error("Scrypted Hugging Face tree pagination exceeded the bound");
401
+ assertScryptedTreePageUrl(new URL(url), revision);
402
+ const response = await opts.fetchImpl(url, {
403
+ redirect: "manual",
404
+ headers: { "User-Agent": "CamStack/1.0" }
405
+ });
406
+ if (response.status === 301 || response.status === 302 || response.status === 303 || response.status === 307 || response.status === 308) throw new ScryptedTreeLinkError(`Refusing tree API redirect ${response.status} (fail-closed, no follow)`);
407
+ if (!response.ok) throw new Error(`HTTP ${response.status} fetching Scrypted Hugging Face tree`);
408
+ const payload = await response.json();
409
+ for (const entry of payload) {
410
+ if (entry.type !== "file" || !entry.path) continue;
411
+ files.push({
412
+ path: entry.path,
413
+ size: entry.size ?? entry.lfs?.size ?? 0,
414
+ ...entry.lfs !== void 0 ? { lfs: entry.lfs } : {}
415
+ });
416
+ }
417
+ url = resolveScryptedTreeNextLink(response.headers.get("link") ?? "", url, revision);
418
+ }
419
+ return files;
420
+ } catch (err) {
421
+ if (err instanceof ScryptedTreeLinkError) throw err;
422
+ lastError = err instanceof Error ? err : new Error(String(err));
423
+ await sleep(Math.min(retryDelayMs * 2 ** attempt, 3e3));
424
+ }
425
+ throw new Error(`Scrypted Hugging Face tree unavailable: ${lastError.message}`);
426
+ }
427
+ //#endregion
428
+ //#region src/scrypted/scrypted-import.ts
429
+ /**
430
+ * Server-owned Scrypted catalog import. The only client input is `manifestId`.
431
+ * Downloads use the pinned host/repo/revision, a bounded staging dir, LFS /
432
+ * size / checksum checks, and atomic finalize with cleanup on every failure.
433
+ */
434
+ var SCRYPTED_IMPORT_MAX_FILE_BYTES = 256 * 1024 * 1024;
435
+ /** Deterministic bundle digest: sorted `path\\0sha256` lines, never the primary file alone. */
436
+ function scryptedBundleFingerprint(entry) {
437
+ const hash = (0, node_crypto.createHash)("sha256");
438
+ const lines = entry.files.map((file) => `${file.relativePath}\0${file.sha256}`).toSorted();
439
+ hash.update(lines.join("\n"));
440
+ return hash.digest("hex");
441
+ }
442
+ var LFS_POINTER_PREFIX = "version https://git-lfs.github.com/spec/v1";
443
+ var SIZE_TOLERANCE = .01;
444
+ function isLfsPointer(filePath) {
445
+ const fd = node_fs.openSync(filePath, "r");
446
+ try {
447
+ const buf = Buffer.alloc(128);
448
+ const n = node_fs.readSync(fd, buf, 0, buf.length, 0);
449
+ return buf.subarray(0, n).toString("utf8").startsWith(LFS_POINTER_PREFIX);
450
+ } finally {
451
+ node_fs.closeSync(fd);
452
+ }
453
+ }
454
+ async function hashFileSha256(filePath) {
455
+ const hash = (0, node_crypto.createHash)("sha256");
456
+ await new Promise((resolve, reject) => {
457
+ node_fs.createReadStream(filePath).on("data", (chunk) => hash.update(chunk)).on("end", () => resolve()).on("error", reject);
458
+ });
459
+ return hash.digest("hex");
460
+ }
461
+ function assertInside(root, dest) {
462
+ const rootResolved = node_path.resolve(root);
463
+ const destResolved = node_path.resolve(dest);
464
+ const prefix = rootResolved.endsWith(node_path.sep) ? rootResolved : `${rootResolved}${node_path.sep}`;
465
+ if (destResolved !== rootResolved && !destResolved.startsWith(prefix)) throw new Error(`Refusing to write outside the destination root: ${dest}`);
466
+ return destResolved;
467
+ }
468
+ function rejectSymlink(filePath) {
469
+ try {
470
+ if (node_fs.lstatSync(filePath).isSymbolicLink()) throw new Error(`Refusing symlink at ${filePath}`);
471
+ } catch (err) {
472
+ if (err.code === "ENOENT") return;
473
+ throw err;
474
+ }
475
+ }
476
+ function sizeOutOfTolerance(actual, declared) {
477
+ if (declared <= 0) return true;
478
+ return Math.abs(actual - declared) / declared > SIZE_TOLERANCE;
479
+ }
480
+ function localBasename(entry) {
481
+ 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`;
482
+ if (entry.format === "openvino") return `${entry.id}.xml`;
483
+ if (entry.format === "tflite") return `${entry.id}.tflite`;
484
+ return entry.files[0]?.relativePath.split("/").pop() ?? `${entry.id}.onnx`;
485
+ }
486
+ function buildImportedDescriptor(entry, localName) {
487
+ const catalog = {
488
+ ...entry.model,
489
+ classMap: SCRYPTED_IDENTITY_CLASS_MAP,
490
+ formats: { ...entry.model.formats }
491
+ };
492
+ const format = entry.format;
493
+ const previous = catalog.formats[format];
494
+ if (format === "coreml") catalog.formats = {
495
+ ...catalog.formats,
496
+ coreml: {
497
+ url: `camstack-local://${entry.id}/${localName}`,
498
+ sizeMB: previous?.sizeMB ?? entry.files.reduce((sum, file) => sum + file.size, 0) / 1e6,
499
+ isDirectory: true,
500
+ files: previous?.files ?? [
501
+ "Manifest.json",
502
+ "Data/com.apple.CoreML/model.mlmodel",
503
+ "Data/com.apple.CoreML/weights/weight.bin"
504
+ ],
505
+ runtimes: ["python"]
506
+ }
507
+ };
508
+ else if (format === "openvino") catalog.formats = {
509
+ ...catalog.formats,
510
+ openvino: {
511
+ url: `camstack-local://${entry.id}/${localName}`,
512
+ sizeMB: previous?.sizeMB ?? entry.files.reduce((sum, file) => sum + file.size, 0) / 1e6,
513
+ files: [`${entry.id}.bin`],
514
+ runtimes: ["python"]
515
+ }
516
+ };
517
+ else catalog.formats = {
518
+ ...catalog.formats,
519
+ [format]: {
520
+ url: `camstack-local://${entry.id}/${localName}`,
521
+ sizeMB: previous?.sizeMB ?? entry.files.reduce((sum, file) => sum + file.size, 0) / 1e6,
522
+ ...previous?.runtimes !== void 0 ? { runtimes: previous.runtimes } : {}
523
+ }
524
+ };
525
+ return {
526
+ stepId: "object-detection",
527
+ entry: catalog
528
+ };
529
+ }
530
+ /** Availability/format selection uses the manifesto format, never key order. */
531
+ function resolveScryptedImportedFormat(manifestId, descriptor) {
532
+ const entry = getScryptedManifestEntry(manifestId);
533
+ if (!entry) throw new Error(`resolveScryptedImportedFormat: unknown manifest id "${manifestId}"`);
534
+ const format = entry.format;
535
+ if (!descriptor.entry.formats[format]) throw new Error(`resolveScryptedImportedFormat: descriptor for "${manifestId}" is missing manifesto format ${format}`);
536
+ return format;
537
+ }
538
+ async function verifyScryptedStagedFile(destPath, file, deps) {
539
+ const maxBytes = deps.maxFileBytes ?? 268435456;
540
+ if (file.size > maxBytes) throw new Error(`Declared size ${file.size} exceeds the ${maxBytes}-byte limit`);
541
+ if (!node_fs.existsSync(destPath)) throw new Error(`missing downloaded file for ${file.relativePath}`);
542
+ rejectSymlink(destPath);
543
+ const actual = node_fs.statSync(destPath).size;
544
+ if (actual > maxBytes) throw new Error(`Downloaded ${actual} bytes exceeds the ${maxBytes}-byte limit`);
545
+ if (isLfsPointer(destPath)) throw new Error(`Downloaded an LFS pointer instead of the artifact (${file.relativePath})`);
546
+ if (sizeOutOfTolerance(actual, file.size)) throw new Error(`Downloaded size ${actual} is outside tolerance of declared ${file.size} for ${file.relativePath}`);
547
+ const expected = assertScryptedFileSha256(file.sha256, file.relativePath);
548
+ if (await (deps.fileSha256 ?? hashFileSha256)(destPath) !== expected) throw new Error(`sha256 checksum mismatch for ${file.relativePath}`);
549
+ }
550
+ async function importScryptedCatalogModel(input, deps) {
551
+ const entry = (deps.resolveEntry ?? getScryptedManifestEntry)(input.manifestId);
552
+ if (!entry) throw new Error(`importScryptedCatalogModel: unknown manifest id "${input.manifestId}"`);
553
+ for (const file of entry.files) assertScryptedFileSha256(file.sha256, file.relativePath);
554
+ const drift = normalizeScryptedDrift(await deps.driftFor(entry.id));
555
+ if (drift.status !== "ok") throw new Error(`importScryptedCatalogModel: entry "${entry.id}" is blocked by drift (${drift.status}${drift.details ? `: ${drift.details}` : ""})`);
556
+ node_fs.mkdirSync(deps.stagingDir, { recursive: true });
557
+ node_fs.mkdirSync(deps.modelsDir, { recursive: true });
558
+ const stagingRoot = node_path.join(deps.stagingDir, `imp-${(0, node_crypto.randomUUID)()}`);
559
+ node_fs.mkdirSync(stagingRoot, { recursive: true });
560
+ try {
561
+ const staged = [];
562
+ for (const file of entry.files) {
563
+ const relative = assertScryptedRelativePath(file.relativePath);
564
+ const dest = assertInside(stagingRoot, node_path.join(stagingRoot, ...relative.split("/")));
565
+ const url = buildScryptedResolveUrl(relative);
566
+ await deps.downloadFile(url, dest);
567
+ await verifyScryptedStagedFile(dest, file, deps);
568
+ staged.push({
569
+ file,
570
+ dest
571
+ });
572
+ }
573
+ if (entry.format === "coreml") {
574
+ const hasManifest = staged.some((row) => row.file.relativePath.endsWith("Manifest.json"));
575
+ const hasModel = staged.some((row) => row.file.relativePath.endsWith("model.mlmodel"));
576
+ const hasWeight = staged.some((row) => row.file.relativePath.endsWith("weight.bin"));
577
+ if (!hasManifest || !hasModel || !hasWeight || staged.length < 3) throw new Error("CoreML payload is incomplete — Manifest.json, model and weights are required");
578
+ }
579
+ if (entry.format === "openvino") {
580
+ const hasXml = staged.some((row) => row.file.relativePath.endsWith(".xml"));
581
+ const hasBin = staged.some((row) => row.file.relativePath.endsWith(".bin"));
582
+ if (!hasXml || !hasBin) throw new Error("OpenVINO payload is incomplete — xml and bin siblings are required");
583
+ }
584
+ const localName = localBasename(entry);
585
+ const finalMain = assertInside(deps.modelsDir, node_path.join(deps.modelsDir, localName));
586
+ rejectSymlink(finalMain);
587
+ if (entry.format === "coreml") {
588
+ const pkgRel = staged.find((row) => row.file.relativePath.endsWith("Manifest.json"))?.file.relativePath.replace(/\/Manifest\.json$/, "");
589
+ if (!pkgRel) throw new Error("CoreML payload is incomplete — Manifest.json missing");
590
+ const stagedPkg = node_path.join(stagingRoot, ...pkgRel.split("/"));
591
+ node_fs.rmSync(finalMain, {
592
+ recursive: true,
593
+ force: true
594
+ });
595
+ node_fs.renameSync(stagedPkg, finalMain);
596
+ } else if (entry.format === "openvino") {
597
+ const xml = staged.find((row) => row.file.relativePath.endsWith(".xml"));
598
+ const bin = staged.find((row) => row.file.relativePath.endsWith(".bin"));
599
+ if (!xml || !bin) throw new Error("OpenVINO payload is incomplete");
600
+ const xmlDest = finalMain;
601
+ const binDest = assertInside(deps.modelsDir, node_path.join(deps.modelsDir, `${entry.id}.bin`));
602
+ node_fs.renameSync(xml.dest, xmlDest);
603
+ node_fs.renameSync(bin.dest, binDest);
604
+ } else {
605
+ const only = staged[0];
606
+ if (!only) throw new Error("payload is incomplete");
607
+ node_fs.renameSync(only.dest, finalMain);
608
+ }
609
+ return buildImportedDescriptor(entry, localName);
610
+ } finally {
611
+ node_fs.rmSync(stagingRoot, {
612
+ recursive: true,
613
+ force: true
614
+ });
615
+ }
616
+ }
617
+ //#endregion
93
618
  //#region ../types/dist/event-category-CIa_iT6b.mjs
94
619
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
95
620
  EventCategory["SystemBoot"] = "system.boot";
@@ -8154,6 +8679,15 @@ var LabelDefinitionSchema = object({
8154
8679
  description: string().optional(),
8155
8680
  icon: string().optional()
8156
8681
  });
8682
+ var ClassMapDefinitionSchema = object({
8683
+ mapping: record(string(), _enum([
8684
+ "person",
8685
+ "vehicle",
8686
+ "animal",
8687
+ "package"
8688
+ ])),
8689
+ preserveOriginal: boolean()
8690
+ });
8157
8691
  var MODEL_FORMATS = [
8158
8692
  "onnx",
8159
8693
  "coreml",
@@ -8332,7 +8866,13 @@ var ModelCatalogEntrySchema = object({
8332
8866
  * `id` stays the source of truth for resolution/download/persistence; grouping
8333
8867
  * is a presentation overlay resolved back to an `id`.
8334
8868
  */
8335
- group: ModelVariantGroupSchema.optional()
8869
+ group: ModelVariantGroupSchema.optional(),
8870
+ /**
8871
+ * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8872
+ * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8873
+ * labels already ARE the CamStack macros (Scrypted identity map).
8874
+ */
8875
+ classMap: ClassMapDefinitionSchema.optional()
8336
8876
  });
8337
8877
  var ConvertTargetSchema = discriminatedUnion("format", [object({
8338
8878
  format: literal("openvino"),
@@ -8361,7 +8901,8 @@ var ModelConvertMetadataSchema = object({
8361
8901
  "ocr",
8362
8902
  "segmentation"
8363
8903
  ]),
8364
- faceAlignment: boolean().optional()
8904
+ faceAlignment: boolean().optional(),
8905
+ classMap: ClassMapDefinitionSchema.optional()
8365
8906
  });
8366
8907
  var ConvertArtifactSchema = object({
8367
8908
  format: _enum(MODEL_FORMATS),
@@ -14541,12 +15082,15 @@ var NcOccupancyConditionSchema = object({
14541
15082
  * there is no second switch that can disagree with the first and every rule
14542
15083
  * authored before the decision migrates for free (`audioModeOf`):
14543
15084
  *
14544
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14545
- * classifier labels with one of them. No window, no percentage:
14546
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14547
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14548
- * the analyzer's (`classificationMinScore`, per device) a label only
14549
- * reaches this condition if the classifier was already confident enough.
15085
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
15086
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
15087
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
15088
+ * frames is the wrong question for a classifier that labels 1–3 frames
15089
+ * per episode. The count window is the brake that drops a single-frame
15090
+ * false positive; the rule's own `throttle` cooldown is the other. The
15091
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
15092
+ * per device) — a label only reaches this condition if the classifier was
15093
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14550
15094
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14551
15095
  * the condition: at least `hitPercent`% of the samples over
14552
15096
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14573,14 +15117,22 @@ var NcOccupancyConditionSchema = object({
14573
15117
  * an operator who typed `dog` mean the same thing.
14574
15118
  */
14575
15119
  var NcAudioConditionSchema = object({
14576
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
15120
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14577
15121
  labels: array(string().min(1)).min(1).optional(),
14578
15122
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14579
15123
  dbThreshold: number().min(-96).max(0).optional(),
14580
15124
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14581
15125
  hitPercent: number().int().min(1).max(100).default(60),
14582
15126
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14583
- samplingSeconds: number().int().min(1).max(300).default(10)
15127
+ samplingSeconds: number().int().min(1).max(300).default(10),
15128
+ /**
15129
+ * LABEL MODE: how many labelled frames must land inside
15130
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
15131
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
15132
+ */
15133
+ confirmHits: number().int().min(1).max(20).optional(),
15134
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
15135
+ confirmWindowSec: number().int().min(1).max(60).optional()
14584
15136
  });
14585
15137
  /**
14586
15138
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -17029,7 +17581,9 @@ var TrackCascadeCountsSchema = object({
17029
17581
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17030
17582
  plates: number().int(),
17031
17583
  /** Per-track CLIP search vectors removed (best-effort). */
17032
- embeddings: number().int()
17584
+ embeddings: number().int(),
17585
+ /** Group membership + group rows removed with their last member (best-effort). */
17586
+ groups: number().int()
17033
17587
  });
17034
17588
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17035
17589
  var DiskReconcileCountsSchema = object({
@@ -17393,6 +17947,33 @@ var NativeCropRefSchema = object({
17393
17947
  h: number()
17394
17948
  })
17395
17949
  });
17950
+ object({
17951
+ crop: object({
17952
+ left: number(),
17953
+ top: number(),
17954
+ width: number().positive(),
17955
+ height: number().positive()
17956
+ }).optional(),
17957
+ content: object({
17958
+ width: number().int().positive(),
17959
+ height: number().int().positive()
17960
+ }),
17961
+ fit: _enum(["stretch", "contain"]),
17962
+ format: _enum([
17963
+ "rgb",
17964
+ "gray",
17965
+ "jpeg"
17966
+ ])
17967
+ });
17968
+ var FrameRefSchema = object({
17969
+ registryId: string().min(1),
17970
+ id: string().min(1),
17971
+ width: number().int().positive(),
17972
+ height: number().int().positive(),
17973
+ format: _enum(["rgb", "gray"]),
17974
+ timestamp: number(),
17975
+ capturedAt: number().optional()
17976
+ });
17396
17977
  var ModelFormatSchema$1 = _enum([
17397
17978
  "onnx",
17398
17979
  "coreml",
@@ -17637,6 +18218,12 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
17637
18218
  steps: array(PipelineStepInputSchema).min(1),
17638
18219
  frame: FrameInputSchema.optional(),
17639
18220
  /**
18221
+ * Process-local lazy frame. Valid only when caller and provider resolve
18222
+ * in the same execution-group process; split/cross-node callers use
18223
+ * `frame`/`image` inline compatibility instead.
18224
+ */
18225
+ frameRef: FrameRefSchema.optional(),
18226
+ /**
17640
18227
  * CB5 shm passthrough — a `FrameHandle` naming the same ring slot
17641
18228
  * the decoded pixels live in. One more member of the one-of
17642
18229
  * frame/frameHandle/image/imageBase64/referenceImage group.
@@ -17892,7 +18479,10 @@ var NativeCropResultSchema = object({
17892
18479
  * Which source served this crop, so a quality-sensitive consumer (the native
17893
18480
  * `keyFrame`) can reject a degraded fallback:
17894
18481
  * - `native` — cut from the decode worker's retained NATIVE surface (the
17895
- * quality path).
18482
+ * quality path). A subject-tile serve is also native-resolution and stays
18483
+ * `native` here: the public enum cannot name `tile` without a breaking cap
18484
+ * change. Runner telemetry distinguishes lease vs tile via `source` on the
18485
+ * internal crop result (`nativeHits` vs `tileHits`).
17896
18486
  * - `ram-fullframe` — the native surface MISSED but the request was full-frame,
17897
18487
  * so the ≤640 RAM `RetainedFrameStore` served it (honest lower-res; legit for
17898
18488
  * the blank-frame guard / 640-snapshot resolve, NOT for the clean keyFrame).
@@ -18383,12 +18973,41 @@ var RunnerLocalLoadSchema = object({
18383
18973
  * legacy `OrchestratorMetricsSchema` shape so existing dashboards keep
18384
18974
  * working unchanged when they switch to reading from the runner cap.
18385
18975
  */
18976
+ var FrameLazyCountersSchema = object({
18977
+ framesDecoded: number(),
18978
+ framesAdmitted: number(),
18979
+ framesDroppedPixelFree: number(),
18980
+ viewsMaterialized: number(),
18981
+ viewsSkipped: number(),
18982
+ workerToRunnerBytes: number(),
18983
+ runnerToPoolRawBytes: number(),
18984
+ runnerToPoolJpegBytes: number(),
18985
+ onDemandFullFrameRequests: number(),
18986
+ onDemandCropRequests: number(),
18987
+ nativeHits: number(),
18988
+ nativeMisses: number(),
18989
+ tileHits: number(),
18990
+ tileMisses: number(),
18991
+ fallbackHits: number(),
18992
+ fallbackMisses: number(),
18993
+ retainedWritesAvoided: number(),
18994
+ residentRefs: number(),
18995
+ residentBytes: number(),
18996
+ releases: number(),
18997
+ evictions: number(),
18998
+ staleMisses: number()
18999
+ });
19000
+ var FrameLazyMetricsSchema = object({
19001
+ node: FrameLazyCountersSchema,
19002
+ cameras: array(FrameLazyCountersSchema.extend({ deviceId: number() }))
19003
+ });
18386
19004
  var RunnerLocalMetricsSchema = object({
18387
19005
  nodeId: string(),
18388
19006
  activeCameras: number(),
18389
19007
  throttledCameras: number(),
18390
19008
  avgInferenceTimeMs: number(),
18391
- queueDepth: number()
19009
+ queueDepth: number(),
19010
+ frameLazy: FrameLazyMetricsSchema.optional()
18392
19011
  });
18393
19012
  method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mutation" }), method(object({ deviceId: number() }), object({ success: literal(true) }), { kind: "mutation" }), method(ReportMotionInputSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), RunnerLocalLoadSchema), method(_void(), RunnerLocalMetricsSchema), method(object({ deviceId: number() }), CameraMetricsSchema.nullable()), method(_void(), array(CameraMetricsWithDeviceIdSchema).readonly()), method(_void(), array(number()).readonly()), method(object({
18394
19013
  handle: FrameHandleSchema,
@@ -19688,6 +20307,9 @@ method(_void(), ProviderInfoSchema), method(object({ config: record(string(), un
19688
20307
  location: StorageLocationSchema,
19689
20308
  relativePath: string()
19690
20309
  }), _void(), { kind: "mutation" }), method(object({ location: StorageLocationSchema }), number().nullable()), method(BeginUploadInputSchema, BeginUploadResultSchema, { kind: "mutation" }), method(WriteChunkInputSchema, _void(), { kind: "mutation" }), method(FinalizeUploadInputSchema, _void(), { kind: "mutation" }), method(AbortUploadInputSchema, _void(), { kind: "mutation" }), method(BeginDownloadInputSchema, BeginDownloadResultSchema, { kind: "mutation" }), method(ReadChunkInputSchema, _instanceof(Uint8Array)), method(EndDownloadInputSchema, _void(), { kind: "mutation" });
20310
+ /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
20311
+ var ProfileSettingsSchemaBridge = unknown().nullable();
20312
+ var ProfileSettingsBagSchema = record(string(), unknown());
19691
20313
  /**
19692
20314
  * A live terminal session hosted by the provider addon. Output and input do
19693
20315
  * NOT flow through the capability — they use the addon data plane
@@ -19717,7 +20339,14 @@ var TerminalSessionInfoSchema = object({
19717
20339
  var TerminalProfileInfoSchema = object({
19718
20340
  profileId: string(),
19719
20341
  label: string(),
19720
- description: string().optional()
20342
+ description: string().optional(),
20343
+ /** Spawn defaults the instance form copies on create. */
20344
+ executable: string().optional(),
20345
+ args: array(string()).readonly().optional(),
20346
+ cwd: string().optional(),
20347
+ environment: array(string()).readonly().optional(),
20348
+ /** ConfigUISchema for instance knobs, or null when the profile has none. */
20349
+ settingsSchema: ProfileSettingsSchemaBridge.optional()
19721
20350
  });
19722
20351
  /**
19723
20352
  * A durable operator-created Terminal instance. Profiles are templates; only
@@ -19730,7 +20359,12 @@ var TerminalInstanceInfoSchema = object({
19730
20359
  profileId: string(),
19731
20360
  profileLabel: string(),
19732
20361
  name: string(),
19733
- enabled: boolean()
20362
+ enabled: boolean(),
20363
+ executable: string(),
20364
+ args: array(string()).readonly(),
20365
+ cwd: string(),
20366
+ environment: array(string()).readonly(),
20367
+ profileSettings: ProfileSettingsBagSchema
19734
20368
  });
19735
20369
  var TerminalLegacyCameraSchema = object({
19736
20370
  stableId: string(),
@@ -19760,7 +20394,23 @@ var TerminalOutputBatchSchema = object({
19760
20394
  method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
19761
20395
  targetNodeId: string().min(1),
19762
20396
  profileId: string().min(1),
19763
- name: string().trim().min(1).max(160).optional()
20397
+ name: string().trim().min(1).max(160).optional(),
20398
+ executable: string().max(1024).optional(),
20399
+ args: array(string().max(2048)).max(64).optional(),
20400
+ cwd: string().max(1024).optional(),
20401
+ environment: array(string().max(4096)).max(64).optional(),
20402
+ profileSettings: ProfileSettingsBagSchema.optional()
20403
+ }), TerminalInstanceInfoSchema, {
20404
+ kind: "mutation",
20405
+ auth: "admin"
20406
+ }), method(object({
20407
+ instanceId: string().min(1),
20408
+ name: string().trim().min(1).max(160).optional(),
20409
+ executable: string().max(1024).optional(),
20410
+ args: array(string().max(2048)).max(64).optional(),
20411
+ cwd: string().max(1024).optional(),
20412
+ environment: array(string().max(4096)).max(64).optional(),
20413
+ profileSettings: ProfileSettingsBagSchema.optional()
19764
20414
  }), TerminalInstanceInfoSchema, {
19765
20415
  kind: "mutation",
19766
20416
  auth: "admin"
@@ -19782,7 +20432,11 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
19782
20432
  }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
19783
20433
  profileId: string(),
19784
20434
  cols: number().int().positive(),
19785
- rows: number().int().positive()
20435
+ rows: number().int().positive(),
20436
+ executable: string().max(1024).optional(),
20437
+ args: array(string().max(2048)).max(64).optional(),
20438
+ cwd: string().max(1024).optional(),
20439
+ environment: array(string().max(4096)).max(64).optional()
19786
20440
  }), TerminalSessionInfoSchema, {
19787
20441
  kind: "mutation",
19788
20442
  auth: "admin"
@@ -22564,10 +23218,10 @@ DeviceType.LawnMower, method(object({ deviceId: number().int().nonnegative() }),
22564
23218
  *
22565
23219
  * • The SDK (mobile / web client) consumes `getConnectionEndpoints()`
22566
23220
  * to receive an ordered list of candidate base URLs it should race
22567
- * on connect — LAN IPv4 first (lowest latency when on same network),
22568
- * then public hostname (if a tunnel is up), then IPv6. The SDK can
22569
- * race them with short timeouts and stick with the winner for the
22570
- * session.
23221
+ * on connect — LAN IPv4 and stable LAN IPv6 first (lowest latency
23222
+ * when on the same network), then public hostname (if a tunnel is
23223
+ * up). The SDK can race them with short timeouts and stick with the
23224
+ * winner for the session.
22571
23225
  *
22572
23226
  * Why hub-only: agents are not directly addressable by the operator's
22573
23227
  * clients — they reverse-connect to the hub. Exposing their interfaces
@@ -22722,6 +23376,17 @@ var NotificationEndpointSchema = object({
22722
23376
  /** What the ranking currently resolves to (null when nothing is reachable). */
22723
23377
  resolved: string().nullable()
22724
23378
  });
23379
+ /**
23380
+ * The URLs the SDK / viewer should race for API access. `baseUrls` empty =
23381
+ * AUTO (every LAN IPv4 + the public tunnel). `resolved` is what that choice
23382
+ * currently expands to, so the UI can show the effective set either way.
23383
+ */
23384
+ var ViewerEndpointsSchema = object({
23385
+ /** The operator's explicit race set, or empty for AUTO. */
23386
+ baseUrls: array(string()).readonly(),
23387
+ /** What the ranking currently resolves to (may be empty if nothing is up). */
23388
+ resolved: array(string()).readonly()
23389
+ });
22725
23390
  var AllowedAddressesSchema = object({
22726
23391
  /**
22727
23392
  * Allowlist of interface addresses operators have explicitly opted
@@ -22730,6 +23395,20 @@ var AllowedAddressesSchema = object({
22730
23395
  * Network Addresses admin page and persisted by the addon.
22731
23396
  */
22732
23397
  addresses: array(string()).readonly() });
23398
+ var TlsStatusSchema = object({
23399
+ mode: _enum([
23400
+ "generated",
23401
+ "uploaded",
23402
+ "disabled"
23403
+ ]),
23404
+ leafFingerprintSha256: string().nullable(),
23405
+ caFingerprintSha256: string().nullable(),
23406
+ validTo: string().nullable(),
23407
+ sans: array(string()),
23408
+ caCertPem: string().nullable(),
23409
+ reissueError: string().nullable(),
23410
+ restartRequired: boolean()
23411
+ });
22733
23412
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
22734
23413
  /**
22735
23414
  * LEGACY HINT — do not send from new code. Kept optional so clients
@@ -22739,17 +23418,31 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
22739
23418
  */
22740
23419
  port: number().int().min(1).max(65535).optional(),
22741
23420
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
22742
- * candidate. Default `true`. */
23421
+ * candidate. Default `false` — loopback is not a client route. */
22743
23422
  includeLoopback: boolean().optional(),
22744
- /** Skip IPv6 entries. Some legacy clients can't parse them.
22745
- * Default `false`. */
23423
+ /** Skip IPv6 entries. Default `false` the palette includes stable
23424
+ * LAN IPv6 (ICE/WebRTC uses dual-stack regardless). Pass `true` to
23425
+ * hide them. The viewer HTTP/WS race is `getViewerEndpoints`. */
22746
23426
  ipv4Only: boolean().optional(),
22747
23427
  /** Scheme to emit for LAN/loopback URLs. Default `'http'`.
22748
23428
  * Pass `'https'` when the caller is itself loaded over HTTPS
22749
23429
  * to avoid mixed-content blocks in the browser. The public
22750
23430
  * tunnel always emits `https://` regardless. */
22751
23431
  scheme: _enum(["http", "https"]).optional()
22752
- }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
23432
+ }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), ViewerEndpointsSchema), method(object({ baseUrls: array(string()).readonly() }), ViewerEndpointsSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" }), method(_void(), TlsStatusSchema), method(object({ reason: string().optional() }), TlsStatusSchema, {
23433
+ kind: "mutation",
23434
+ auth: "admin"
23435
+ }), method(object({
23436
+ certPem: string().min(1),
23437
+ keyPem: string().min(1),
23438
+ caPem: string().optional()
23439
+ }), TlsStatusSchema, {
23440
+ kind: "mutation",
23441
+ auth: "admin"
23442
+ }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
23443
+ kind: "mutation",
23444
+ auth: "admin"
23445
+ });
22753
23446
  object({
22754
23447
  /** Lifecycle state of the lock. `jammed` means the motor reported
22755
23448
  * failure to reach the target — operator intervention required. */
@@ -28512,6 +29205,12 @@ Object.freeze({
28512
29205
  addonId: null,
28513
29206
  access: "create"
28514
29207
  },
29208
+ "localNetwork.downloadCa": {
29209
+ capName: "local-network",
29210
+ capScope: "system",
29211
+ addonId: null,
29212
+ access: "view"
29213
+ },
28515
29214
  "localNetwork.getAllowedAddresses": {
28516
29215
  capName: "local-network",
28517
29216
  capScope: "system",
@@ -28536,18 +29235,42 @@ Object.freeze({
28536
29235
  addonId: null,
28537
29236
  access: "view"
28538
29237
  },
29238
+ "localNetwork.getTlsStatus": {
29239
+ capName: "local-network",
29240
+ capScope: "system",
29241
+ addonId: null,
29242
+ access: "view"
29243
+ },
29244
+ "localNetwork.getViewerEndpoints": {
29245
+ capName: "local-network",
29246
+ capScope: "system",
29247
+ addonId: null,
29248
+ access: "view"
29249
+ },
28539
29250
  "localNetwork.list": {
28540
29251
  capName: "local-network",
28541
29252
  capScope: "system",
28542
29253
  addonId: null,
28543
29254
  access: "view"
28544
29255
  },
29256
+ "localNetwork.regenerateCertificate": {
29257
+ capName: "local-network",
29258
+ capScope: "system",
29259
+ addonId: null,
29260
+ access: "create"
29261
+ },
28545
29262
  "localNetwork.resetAllowlistToBestMatch": {
28546
29263
  capName: "local-network",
28547
29264
  capScope: "system",
28548
29265
  addonId: null,
28549
29266
  access: "delete"
28550
29267
  },
29268
+ "localNetwork.revertToGeneratedCertificate": {
29269
+ capName: "local-network",
29270
+ capScope: "system",
29271
+ addonId: null,
29272
+ access: "create"
29273
+ },
28551
29274
  "localNetwork.setAllowedAddresses": {
28552
29275
  capName: "local-network",
28553
29276
  capScope: "system",
@@ -28560,6 +29283,18 @@ Object.freeze({
28560
29283
  addonId: null,
28561
29284
  access: "create"
28562
29285
  },
29286
+ "localNetwork.setViewerEndpoints": {
29287
+ capName: "local-network",
29288
+ capScope: "system",
29289
+ addonId: null,
29290
+ access: "create"
29291
+ },
29292
+ "localNetwork.uploadCertificate": {
29293
+ capName: "local-network",
29294
+ capScope: "system",
29295
+ addonId: null,
29296
+ access: "create"
29297
+ },
28563
29298
  "lockControl.lock": {
28564
29299
  capName: "lock-control",
28565
29300
  capScope: "device",
@@ -31440,6 +32175,12 @@ Object.freeze({
31440
32175
  addonId: null,
31441
32176
  access: "create"
31442
32177
  },
32178
+ "terminalSession.updateInstance": {
32179
+ capName: "terminal-session",
32180
+ capScope: "system",
32181
+ addonId: null,
32182
+ access: "create"
32183
+ },
31443
32184
  "terminalSession.writeInput": {
31444
32185
  capName: "terminal-session",
31445
32186
  capScope: "system",
@@ -33927,6 +34668,35 @@ Object.freeze(Object.fromEntries([{
33927
34668
  }]
33928
34669
  }].map((s) => [s.stepId, s.defaultModelId])));
33929
34670
  string().min(1);
34671
+ var CLUSTER_STEP_SETTING_FIELDS = [{
34672
+ stepId: "face-embedding",
34673
+ key: "minLandmarkFaceSize",
34674
+ label: "Min face size for recognition (detection px)",
34675
+ description: "Refuse to embed a face smaller than this IN THE DETECTION FRAME. Applies to every node — the enrolled gallery is one index, and two admission floors would fill it from two policies. 0 disables the gate.",
34676
+ type: "slider",
34677
+ min: 0,
34678
+ max: 64,
34679
+ step: 2,
34680
+ default: 24
34681
+ }];
34682
+ function clusterStepSettingKey(stepId, fieldKey) {
34683
+ return `clusterStepSetting:${stepId}:${fieldKey}`;
34684
+ }
34685
+ var ClusterSettingNumberSchema = number().finite();
34686
+ function readClusterStepSettings(config) {
34687
+ const out = {};
34688
+ for (const field of CLUSTER_STEP_SETTING_FIELDS) {
34689
+ const parsed = ClusterSettingNumberSchema.safeParse(config[clusterStepSettingKey(field.stepId, field.key)]);
34690
+ const value = parsed.success ? parsed.data : field.default;
34691
+ const existing = out[field.stepId] ?? {};
34692
+ out[field.stepId] = {
34693
+ ...existing,
34694
+ [field.key]: value
34695
+ };
34696
+ }
34697
+ return out;
34698
+ }
34699
+ readClusterStepSettings({});
33930
34700
  object({
33931
34701
  /**
33932
34702
  * Fraction of the box's own size added on EACH side before cutting.
@@ -34176,7 +34946,8 @@ function buildEntry(metadata, artifacts) {
34176
34946
  ...metadata.inputLayout !== void 0 ? { inputLayout: metadata.inputLayout } : {},
34177
34947
  ...metadata.inputNormalization !== void 0 ? { inputNormalization: metadata.inputNormalization } : {},
34178
34948
  ...metadata.preprocessMode !== void 0 ? { preprocessMode: metadata.preprocessMode } : {},
34179
- ...metadata.faceAlignment !== void 0 ? { faceAlignment: metadata.faceAlignment } : {}
34949
+ ...metadata.faceAlignment !== void 0 ? { faceAlignment: metadata.faceAlignment } : {},
34950
+ ...metadata.classMap !== void 0 ? { classMap: metadata.classMap } : {}
34180
34951
  };
34181
34952
  }
34182
34953
  function buildModelConvertProvider(deps) {
@@ -34713,6 +35484,45 @@ var ModelNodeAvailabilitySchema = object({
34713
35484
  });
34714
35485
  /** `modelId → nodeId → availability`. */
34715
35486
  var ModelAvailabilityMapSchema = record(string(), record(string(), ModelNodeAvailabilitySchema));
35487
+ var ScryptedManifestFileSchema = object({
35488
+ relativePath: string(),
35489
+ size: number(),
35490
+ sha256: string().regex(/^[0-9a-f]{64}$/)
35491
+ });
35492
+ var ScryptedManifestEntrySchema = object({
35493
+ id: string(),
35494
+ label: string(),
35495
+ family: _enum([
35496
+ "yolov9t",
35497
+ "yolov9s",
35498
+ "yolov9m",
35499
+ "yolov9c"
35500
+ ]),
35501
+ variant: _enum(["relu", "relu_test"]),
35502
+ format: _enum([
35503
+ "coreml",
35504
+ "onnx",
35505
+ "openvino",
35506
+ "tflite"
35507
+ ]),
35508
+ precision: string(),
35509
+ inputSize: object({
35510
+ width: number(),
35511
+ height: number()
35512
+ }),
35513
+ files: array(ScryptedManifestFileSchema).readonly(),
35514
+ model: ModelCatalogEntrySchema
35515
+ });
35516
+ var ScryptedCatalogDriftSchema = object({
35517
+ manifestId: string(),
35518
+ status: _enum([
35519
+ "ok",
35520
+ "missing",
35521
+ "size-changed",
35522
+ "unavailable"
35523
+ ]),
35524
+ details: string().optional()
35525
+ });
34716
35526
  var modelStudioActions = defineCustomActions({
34717
35527
  registerCustomModel: customAction(CustomModelDescriptorSchema, object({ ok: literal(true) }), { kind: "mutation" }),
34718
35528
  removeCustomModel: customAction(object({ modelId: string() }), object({ removed: boolean() }), { kind: "mutation" }),
@@ -34779,6 +35589,16 @@ var modelStudioActions = defineCustomActions({
34779
35589
  bytes: number()
34780
35590
  }), { kind: "mutation" }),
34781
35591
  /**
35592
+ * Local Scrypted YOLO catalog (Task 7B). Listing is a projection of the
35593
+ * revision-pinned manifesto plus a read-only HF drift check. Import accepts
35594
+ * ONLY `manifestId` — host/repo/revision/path/labels/checksum stay server-owned.
35595
+ */
35596
+ listScryptedCatalog: customAction(_void(), object({
35597
+ entries: array(ScryptedManifestEntrySchema).readonly(),
35598
+ drift: array(ScryptedCatalogDriftSchema).readonly()
35599
+ })),
35600
+ importScryptedCatalogModel: customAction(object({ manifestId: string() }), CustomModelDescriptorSchema, { kind: "mutation" }),
35601
+ /**
34782
35602
  * Frigate+ personal models. The API key lives in the addon's SETTINGS
34783
35603
  * (password field) and never leaves the hub: list / test / import all run
34784
35604
  * server-side against api.frigate.video. Status deliberately reveals only
@@ -35043,6 +35863,8 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
35043
35863
  notes: c.notes
35044
35864
  })),
35045
35865
  importFrigateCatalogModel: async (input) => this.importFrigateCatalogModel(input),
35866
+ listScryptedCatalog: async () => this.listScryptedCatalog(),
35867
+ importScryptedCatalogModel: async (input) => this.importScryptedCatalogFromManifest(input),
35046
35868
  tfliteSupport: async () => {
35047
35869
  const api = this.ctx.api;
35048
35870
  if (!api) return { nodes: [] };
@@ -35235,6 +36057,78 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
35235
36057
  return hash.digest("hex");
35236
36058
  }
35237
36059
  /**
36060
+ * Projection of the local Scrypted YOLO manifesto plus a read-only HF drift
36061
+ * check. Network errors become `unavailable` rows — the manifesto is never
36062
+ * mutated and nothing is auto-imported.
36063
+ */
36064
+ async listScryptedCatalog() {
36065
+ try {
36066
+ return {
36067
+ entries: SCRYPTED_YOLO_MANIFEST,
36068
+ drift: compareScryptedManifestDrift(SCRYPTED_YOLO_MANIFEST, await fetchScryptedRemoteTree({ fetchImpl: fetch })).map(normalizeScryptedDrift)
36069
+ };
36070
+ } catch (err) {
36071
+ const details = err instanceof Error ? err.message : "Hugging Face tree unavailable";
36072
+ return {
36073
+ entries: SCRYPTED_YOLO_MANIFEST,
36074
+ drift: SCRYPTED_YOLO_MANIFEST.map((entry) => ({
36075
+ manifestId: entry.id,
36076
+ status: "unavailable",
36077
+ details
36078
+ }))
36079
+ };
36080
+ }
36081
+ }
36082
+ /**
36083
+ * Import one manifesto entry by id. Host/repo/revision/path stay server-owned.
36084
+ */
36085
+ async importScryptedCatalogFromManifest(input) {
36086
+ const modelsDir = await this.resolveModelsDir();
36087
+ const descriptor = await importScryptedCatalogModel(input, {
36088
+ stagingDir: node_path.default.join(this.ctx.dataDir ?? node_os.default.tmpdir(), "scrypted-import"),
36089
+ modelsDir,
36090
+ downloadFile: (url, destPath) => downloadFile(url, destPath, {
36091
+ redirectPolicy: assertScryptedDownloadUrl,
36092
+ maxBytes: SCRYPTED_IMPORT_MAX_FILE_BYTES,
36093
+ timeoutMs: 6e4
36094
+ }),
36095
+ driftFor: async (manifestId) => {
36096
+ try {
36097
+ const tree = await fetchScryptedRemoteTree({ fetchImpl: fetch });
36098
+ const row = compareScryptedManifestDrift(SCRYPTED_YOLO_MANIFEST.filter((entry) => entry.id === manifestId), tree)[0];
36099
+ return normalizeScryptedDrift(row ?? {
36100
+ manifestId,
36101
+ status: "missing",
36102
+ details: "entry missing from manifesto"
36103
+ });
36104
+ } catch (err) {
36105
+ return {
36106
+ manifestId,
36107
+ status: "unavailable",
36108
+ details: err instanceof Error ? err.message : "Hugging Face tree unavailable"
36109
+ };
36110
+ }
36111
+ }
36112
+ });
36113
+ await this.registerCustomModel(descriptor);
36114
+ const format = resolveScryptedImportedFormat(input.manifestId, descriptor);
36115
+ const manifestEntry = getScryptedManifestEntry(input.manifestId);
36116
+ if (!manifestEntry) throw new Error(`Imported Scrypted catalog model is missing manifesto entry ${input.manifestId}`);
36117
+ const bytes = Math.round((descriptor.entry.formats[format]?.sizeMB ?? 0) * 1e6);
36118
+ await this.availability_.update((prev) => upsertAvailability(prev, descriptor.entry.id, "hub", {
36119
+ format,
36120
+ sha256: scryptedBundleFingerprint(manifestEntry),
36121
+ bytes,
36122
+ at: Date.now()
36123
+ }));
36124
+ this.ctx.logger.info("Imported Scrypted catalog model", { meta: {
36125
+ manifestId: input.manifestId,
36126
+ modelId: descriptor.entry.id,
36127
+ bytes
36128
+ } });
36129
+ return descriptor;
36130
+ }
36131
+ /**
35238
36132
  * Import a curated PUBLIC Frigate catalog model: download SERVER-SIDE into
35239
36133
  * the hub's modelsDir (size-verified against the curated declaration),
35240
36134
  * register the descriptor with the PUBLIC url (nodes download on demand),