@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
@@ -11,7 +11,7 @@ import { brotliCompress, gzip } from "node:zlib";
11
11
  //#region \0rolldown/runtime.js
12
12
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
13
13
  //#endregion
14
- //#region ../system/dist/file-data-plane-CuE_hBli.mjs
14
+ //#region ../system/dist/file-data-plane-BhKdxJgf.mjs
15
15
  /** Build fetch headers, including HF auth token for huggingface.co URLs */
16
16
  function buildHeaders(url) {
17
17
  const headers = { "User-Agent": "CamStack/1.0" };
@@ -19,21 +19,56 @@ function buildHeaders(url) {
19
19
  if (hfToken && url.includes("huggingface.co")) headers["Authorization"] = `Bearer ${hfToken}`;
20
20
  return headers;
21
21
  }
22
- /**
23
- * Download a single file from a URL to a destination path.
24
- * Uses native fetch() (Node 22+) which handles redirects natively.
25
- * Streams to disk with optional progress callback.
26
- * Returns the destination path. Skips download if file already exists.
27
- */
28
- async function downloadFile(url, destPath, onProgress) {
22
+ var DEFAULT_MAX_REDIRECTS = 5;
23
+ function normalizeDownloadOptions(third) {
24
+ if (typeof third === "function") return { onProgress: third };
25
+ return third ?? {};
26
+ }
27
+ function isRedirectStatus(status) {
28
+ return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
29
+ }
30
+ function resolveRedirectUrl(current, location) {
31
+ return new URL(location, current);
32
+ }
33
+ async function downloadFile(url, destPath, onProgressOrOptions) {
29
34
  if (fs$1.existsSync(destPath)) return destPath;
35
+ const opts = normalizeDownloadOptions(onProgressOrOptions);
36
+ const fetchImpl = opts.fetchImpl ?? fetch;
37
+ const maxRedirects = opts.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
30
38
  fs$1.mkdirSync(path$1.dirname(destPath), { recursive: true });
31
39
  const tmpPath = destPath + ".downloading";
32
40
  try {
33
- const response = await fetch(url, {
34
- redirect: "follow",
35
- headers: buildHeaders(url)
36
- });
41
+ let current = url;
42
+ const seen = /* @__PURE__ */ new Set();
43
+ let response;
44
+ const manual = opts.redirectPolicy !== void 0;
45
+ for (let hop = 0; hop <= maxRedirects; hop++) {
46
+ const parsed = new URL(current);
47
+ if (opts.redirectPolicy) opts.redirectPolicy(parsed, hop);
48
+ if (seen.has(parsed.href)) throw new Error(`Redirect loop detected at ${parsed.href}`);
49
+ seen.add(parsed.href);
50
+ const controller = opts.timeoutMs !== void 0 ? new AbortController() : void 0;
51
+ const timer = controller && opts.timeoutMs !== void 0 ? setTimeout(() => controller.abort(), opts.timeoutMs) : void 0;
52
+ try {
53
+ response = await fetchImpl(current, {
54
+ redirect: manual ? "manual" : "follow",
55
+ headers: buildHeaders(current),
56
+ ...controller ? { signal: controller.signal } : {}
57
+ });
58
+ } finally {
59
+ if (timer) clearTimeout(timer);
60
+ }
61
+ if (manual && isRedirectStatus(response.status)) {
62
+ const location = response.headers.get("location");
63
+ if (!location) throw new Error(`Redirect ${response.status} missing Location header`);
64
+ if (hop >= maxRedirects) throw new Error(`Too many redirects (max ${maxRedirects}) downloading ${url}`);
65
+ current = resolveRedirectUrl(current, location).href;
66
+ continue;
67
+ }
68
+ break;
69
+ }
70
+ if (!response) throw new Error(`No response downloading ${url}`);
71
+ if (manual && isRedirectStatus(response.status)) throw new Error(`Too many redirects (max ${maxRedirects}) downloading ${url}`);
37
72
  if (!response.ok) throw new Error(`HTTP ${response.status} downloading ${url}`);
38
73
  if (!response.body) throw new Error(`No response body from ${url}`);
39
74
  const total = parseInt(response.headers.get("content-length") ?? "0", 10);
@@ -44,9 +79,10 @@ async function downloadFile(url, destPath, onProgress) {
44
79
  for (;;) {
45
80
  const { done, value } = await reader.read();
46
81
  if (done || !value) break;
47
- fileStream.write(value);
48
82
  downloaded += value.length;
49
- onProgress?.(downloaded, total);
83
+ if (opts.maxBytes !== void 0 && downloaded > opts.maxBytes) throw new Error(`Downloaded ${downloaded} bytes exceeds the ${opts.maxBytes}-byte limit`);
84
+ fileStream.write(value);
85
+ opts.onProgress?.(downloaded, total);
50
86
  }
51
87
  } finally {
52
88
  fileStream.end();
@@ -67,6 +103,495 @@ async function downloadFile(url, destPath, onProgress) {
67
103
  promisify(brotliCompress);
68
104
  promisify(gzip);
69
105
  //#endregion
106
+ //#region src/scrypted/scrypted-download-policy.ts
107
+ /**
108
+ * Strict per-hop allow-list for Scrypted catalog downloads.
109
+ *
110
+ * Hosts are the ones observed from the pinned revision
111
+ * `187402b20a3be0a7fab9bceee21d857ed6ae0064` — not a generic CDN wildcard.
112
+ */
113
+ var SCRYPTED_DOWNLOAD_REDIRECT_HOSTS = ["huggingface.co", "us.aws.cdn.hf.co"];
114
+ var LOCAL_HOSTS = new Set([
115
+ "localhost",
116
+ "127.0.0.1",
117
+ "::1",
118
+ "0.0.0.0"
119
+ ]);
120
+ var IPV4_RE = /^(?:\d{1,3}\.){3}\d{1,3}$/;
121
+ function assertScryptedDownloadUrl(url, _hop) {
122
+ if (url.protocol !== "https:") throw new Error(`http downgrade refused: ${url.protocol}`);
123
+ if (url.username || url.password) throw new Error("userinfo not allowed on download URL");
124
+ const host = url.hostname.toLowerCase();
125
+ if (LOCAL_HOSTS.has(host) || IPV4_RE.test(host) || host.includes(":")) throw new Error(`host not allowed: ${host}`);
126
+ if (!SCRYPTED_DOWNLOAD_REDIRECT_HOSTS.includes(host)) throw new Error(`host not allowed: ${host}`);
127
+ }
128
+ //#endregion
129
+ //#region src/scrypted/scrypted-yolo-manifest-data.json
130
+ 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}]");
131
+ //#endregion
132
+ //#region src/scrypted/scrypted-yolo-manifest.ts
133
+ var SCRYPTED_SHA256_RE = /^[0-9a-f]{64}$/;
134
+ var SCRYPTED_HF_HOST = "https://huggingface.co";
135
+ var SCRYPTED_HF_REPO = "scrypted/plugin-models";
136
+ var SCRYPTED_HF_REVISION = "187402b20a3be0a7fab9bceee21d857ed6ae0064";
137
+ var SCRYPTED_YOLO_LABELS = [
138
+ "person",
139
+ "vehicle",
140
+ "animal"
141
+ ];
142
+ var SCRYPTED_IDENTITY_CLASS_MAP = {
143
+ mapping: {
144
+ person: "person",
145
+ vehicle: "vehicle",
146
+ animal: "animal"
147
+ },
148
+ preserveOriginal: false
149
+ };
150
+ var SCRYPTED_LABEL_DEFS = SCRYPTED_YOLO_LABELS.map((id) => ({
151
+ id,
152
+ name: id
153
+ }));
154
+ var MLPACKAGE_INNER_FILES = [
155
+ "Manifest.json",
156
+ "Data/com.apple.CoreML/model.mlmodel",
157
+ "Data/com.apple.CoreML/weights/weight.bin"
158
+ ];
159
+ var INPUT_SIZE = {
160
+ width: 320,
161
+ height: 320
162
+ };
163
+ function assertScryptedFileSha256(value, relativePath) {
164
+ if (value === void 0 || !SCRYPTED_SHA256_RE.test(value)) throw new Error(`Invalid sha256 for ${relativePath}: expected 64 lowercase hex`);
165
+ return value;
166
+ }
167
+ function assertScryptedRelativePath(value) {
168
+ if (value.length === 0 || value.startsWith("/") || value.includes("\\") || value.includes("\0")) throw new Error(`Unsafe Scrypted relative path: ${value}`);
169
+ if (value.split("/").some((segment) => segment.length === 0 || segment === "." || segment === "..")) throw new Error(`Unsafe Scrypted relative path: ${value}`);
170
+ return value;
171
+ }
172
+ function buildScryptedResolveUrl(relativePath) {
173
+ return `${SCRYPTED_HF_HOST}/${SCRYPTED_HF_REPO}/resolve/${SCRYPTED_HF_REVISION}/${assertScryptedRelativePath(relativePath).split("/").map((segment) => encodeURIComponent(segment)).join("/")}`;
174
+ }
175
+ function packageDirectory(files) {
176
+ const manifest = files.find((file) => file.relativePath.endsWith("/Manifest.json"));
177
+ if (!manifest) throw new Error("CoreML entry is missing Manifest.json");
178
+ return assertScryptedRelativePath(manifest.relativePath.replace(/\/Manifest\.json$/, ""));
179
+ }
180
+ function buildModel(spec) {
181
+ const group = {
182
+ family: "yolov9",
183
+ tier: spec.family.slice(-1),
184
+ resolution: 320,
185
+ ...spec.variant === "relu_test" ? { optimization: "fast" } : {},
186
+ ...spec.precision === "int8" ? { precision: "int8" } : {}
187
+ };
188
+ if (spec.format === "coreml") {
189
+ const dir = packageDirectory(spec.files);
190
+ return {
191
+ id: spec.id,
192
+ name: spec.label,
193
+ description: `${spec.label} — imported from the local Scrypted YOLO catalog.`,
194
+ formats: { coreml: {
195
+ url: buildScryptedResolveUrl(dir),
196
+ sizeMB: spec.sizeMB,
197
+ isDirectory: true,
198
+ files: [...MLPACKAGE_INNER_FILES],
199
+ runtimes: ["python"]
200
+ } },
201
+ inputSize: INPUT_SIZE,
202
+ labels: SCRYPTED_LABEL_DEFS,
203
+ preprocessMode: "resize",
204
+ classMap: SCRYPTED_IDENTITY_CLASS_MAP,
205
+ group
206
+ };
207
+ }
208
+ if (spec.format === "openvino") {
209
+ const xml = spec.files.find((file) => file.relativePath.endsWith(".xml"));
210
+ const bin = spec.files.find((file) => file.relativePath.endsWith(".bin"));
211
+ if (!xml || !bin) throw new Error(`OpenVINO entry ${spec.id} is missing xml/bin`);
212
+ const binName = bin.relativePath.split("/").pop() ?? "best-converted.bin";
213
+ return {
214
+ id: spec.id,
215
+ name: spec.label,
216
+ description: `${spec.label} — imported from the local Scrypted YOLO catalog.`,
217
+ formats: { openvino: {
218
+ url: buildScryptedResolveUrl(xml.relativePath),
219
+ sizeMB: spec.sizeMB,
220
+ files: [binName],
221
+ runtimes: ["python"]
222
+ } },
223
+ inputSize: INPUT_SIZE,
224
+ labels: SCRYPTED_LABEL_DEFS,
225
+ preprocessMode: "resize",
226
+ classMap: SCRYPTED_IDENTITY_CLASS_MAP,
227
+ group
228
+ };
229
+ }
230
+ const main = spec.files[0];
231
+ if (!main) throw new Error(`Entry ${spec.id} has no files`);
232
+ return {
233
+ id: spec.id,
234
+ name: spec.label,
235
+ description: `${spec.label} — imported from the local Scrypted YOLO catalog.`,
236
+ formats: { [spec.format]: {
237
+ url: buildScryptedResolveUrl(main.relativePath),
238
+ sizeMB: spec.sizeMB,
239
+ runtimes: spec.format === "tflite" ? ["python"] : void 0
240
+ } },
241
+ inputSize: INPUT_SIZE,
242
+ labels: SCRYPTED_LABEL_DEFS,
243
+ preprocessMode: "resize",
244
+ classMap: SCRYPTED_IDENTITY_CLASS_MAP,
245
+ group
246
+ };
247
+ }
248
+ function freezeEntry(spec) {
249
+ const files = spec.files.map((file) => Object.freeze({
250
+ relativePath: assertScryptedRelativePath(file.relativePath),
251
+ size: file.size,
252
+ sha256: assertScryptedFileSha256(file.sha256, file.relativePath)
253
+ }));
254
+ return Object.freeze({
255
+ id: spec.id,
256
+ label: spec.label,
257
+ family: spec.family,
258
+ variant: spec.variant,
259
+ format: spec.format,
260
+ precision: spec.precision,
261
+ inputSize: INPUT_SIZE,
262
+ files: Object.freeze(files),
263
+ model: buildModel({
264
+ ...spec,
265
+ files
266
+ })
267
+ });
268
+ }
269
+ var SCRYPTED_YOLO_MANIFEST = Object.freeze(scrypted_yolo_manifest_data_default.map(freezeEntry));
270
+ function getScryptedManifestEntry(manifestId) {
271
+ return SCRYPTED_YOLO_MANIFEST.find((entry) => entry.id === manifestId);
272
+ }
273
+ //#endregion
274
+ //#region src/scrypted/scrypted-tree-drift.ts
275
+ /**
276
+ * Read-only drift check: compare the local Scrypted YOLO manifest to a
277
+ * Hugging Face tree. Never mutates the manifest and never auto-imports
278
+ * remote-only files.
279
+ */
280
+ var SCRYPTED_KNOWN_DRIFT_STATUSES = [
281
+ "ok",
282
+ "missing",
283
+ "size-changed",
284
+ "unavailable"
285
+ ];
286
+ /** Fail-closed: a future/unknown status is never treated as `ok`. */
287
+ function normalizeScryptedDrift(row) {
288
+ if (SCRYPTED_KNOWN_DRIFT_STATUSES.includes(row.status)) return {
289
+ manifestId: row.manifestId,
290
+ status: row.status,
291
+ ...row.details !== void 0 ? { details: row.details } : {}
292
+ };
293
+ return {
294
+ manifestId: row.manifestId,
295
+ status: "unavailable",
296
+ details: row.details ?? `unknown drift status: ${row.status}`
297
+ };
298
+ }
299
+ function remoteSize(file) {
300
+ return file.lfs?.size ?? file.size;
301
+ }
302
+ function compareScryptedManifestDrift(manifest, tree) {
303
+ const byPath = new Map(tree.map((file) => [file.path, file]));
304
+ return manifest.map((entry) => {
305
+ const missing = [];
306
+ const changed = [];
307
+ for (const file of entry.files) {
308
+ const remote = byPath.get(file.relativePath);
309
+ if (!remote) {
310
+ missing.push(file.relativePath);
311
+ continue;
312
+ }
313
+ if (remoteSize(remote) !== file.size) changed.push(`${file.relativePath} declared ${file.size} remote ${remoteSize(remote)}`);
314
+ }
315
+ if (missing.length > 0) return {
316
+ manifestId: entry.id,
317
+ status: "missing",
318
+ details: `missing: ${missing.join(", ")}`
319
+ };
320
+ if (changed.length > 0) return {
321
+ manifestId: entry.id,
322
+ status: "size-changed",
323
+ details: `size-changed: ${changed.join(", ")}`
324
+ };
325
+ return {
326
+ manifestId: entry.id,
327
+ status: "ok"
328
+ };
329
+ });
330
+ }
331
+ var ScryptedTreeLinkError = class extends Error {
332
+ constructor(message) {
333
+ super(message);
334
+ this.name = "ScryptedTreeLinkError";
335
+ }
336
+ };
337
+ function assertScryptedTreePageUrl(url, revision) {
338
+ if (url.protocol !== "https:") throw new ScryptedTreeLinkError(`Refusing non-https tree link: ${url.protocol}`);
339
+ if (url.username || url.password) throw new ScryptedTreeLinkError("Refusing tree link with userinfo");
340
+ if (url.origin !== `https://huggingface.co`) throw new ScryptedTreeLinkError(`Refusing tree link origin ${url.origin}`);
341
+ const expectedPath = `/api/models/${SCRYPTED_HF_REPO}/tree/${revision}`;
342
+ if (url.pathname !== expectedPath) throw new ScryptedTreeLinkError(`Refusing tree link path/revision ${url.pathname} (expected ${expectedPath})`);
343
+ }
344
+ /**
345
+ * Parse `Link: <...>; rel="next"` and accept only an HTTPS huggingface.co
346
+ * tree URL for the same repo + revision. Relative hrefs resolve against the
347
+ * current page URL. Never returns a URL that failed validation.
348
+ */
349
+ function resolveScryptedTreeNextLink(header, currentUrl, revision) {
350
+ if (!header) return void 0;
351
+ const match = /<([^>]+)>;\s*rel="next"/.exec(header);
352
+ if (!match?.[1]) return void 0;
353
+ let resolved;
354
+ try {
355
+ resolved = new URL(match[1], currentUrl);
356
+ } catch {
357
+ throw new ScryptedTreeLinkError(`Refusing unparsable tree next link: ${match[1]}`);
358
+ }
359
+ assertScryptedTreePageUrl(resolved, revision);
360
+ return resolved.href;
361
+ }
362
+ async function sleep(ms) {
363
+ if (ms <= 0) return;
364
+ await new Promise((resolve) => setTimeout(resolve, ms));
365
+ }
366
+ async function fetchScryptedRemoteTree(opts) {
367
+ const revision = opts.revision ?? "main";
368
+ const maxRetries = opts.maxRetries ?? 4;
369
+ const retryDelayMs = opts.retryDelayMs ?? 200;
370
+ let lastError = /* @__PURE__ */ new Error("Scrypted Hugging Face tree unavailable");
371
+ for (let attempt = 0; attempt < maxRetries; attempt++) try {
372
+ const files = [];
373
+ let url = `${SCRYPTED_HF_HOST}/api/models/${SCRYPTED_HF_REPO}/tree/${encodeURIComponent(revision)}?recursive=true&expand=true`;
374
+ let pages = 0;
375
+ while (url) {
376
+ pages += 1;
377
+ if (pages > 50) throw new Error("Scrypted Hugging Face tree pagination exceeded the bound");
378
+ assertScryptedTreePageUrl(new URL(url), revision);
379
+ const response = await opts.fetchImpl(url, {
380
+ redirect: "manual",
381
+ headers: { "User-Agent": "CamStack/1.0" }
382
+ });
383
+ 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)`);
384
+ if (!response.ok) throw new Error(`HTTP ${response.status} fetching Scrypted Hugging Face tree`);
385
+ const payload = await response.json();
386
+ for (const entry of payload) {
387
+ if (entry.type !== "file" || !entry.path) continue;
388
+ files.push({
389
+ path: entry.path,
390
+ size: entry.size ?? entry.lfs?.size ?? 0,
391
+ ...entry.lfs !== void 0 ? { lfs: entry.lfs } : {}
392
+ });
393
+ }
394
+ url = resolveScryptedTreeNextLink(response.headers.get("link") ?? "", url, revision);
395
+ }
396
+ return files;
397
+ } catch (err) {
398
+ if (err instanceof ScryptedTreeLinkError) throw err;
399
+ lastError = err instanceof Error ? err : new Error(String(err));
400
+ await sleep(Math.min(retryDelayMs * 2 ** attempt, 3e3));
401
+ }
402
+ throw new Error(`Scrypted Hugging Face tree unavailable: ${lastError.message}`);
403
+ }
404
+ //#endregion
405
+ //#region src/scrypted/scrypted-import.ts
406
+ /**
407
+ * Server-owned Scrypted catalog import. The only client input is `manifestId`.
408
+ * Downloads use the pinned host/repo/revision, a bounded staging dir, LFS /
409
+ * size / checksum checks, and atomic finalize with cleanup on every failure.
410
+ */
411
+ var SCRYPTED_IMPORT_MAX_FILE_BYTES = 256 * 1024 * 1024;
412
+ /** Deterministic bundle digest: sorted `path\\0sha256` lines, never the primary file alone. */
413
+ function scryptedBundleFingerprint(entry) {
414
+ const hash = createHash("sha256");
415
+ const lines = entry.files.map((file) => `${file.relativePath}\0${file.sha256}`).toSorted();
416
+ hash.update(lines.join("\n"));
417
+ return hash.digest("hex");
418
+ }
419
+ var LFS_POINTER_PREFIX = "version https://git-lfs.github.com/spec/v1";
420
+ var SIZE_TOLERANCE = .01;
421
+ function isLfsPointer(filePath) {
422
+ const fd = fs$1.openSync(filePath, "r");
423
+ try {
424
+ const buf = Buffer.alloc(128);
425
+ const n = fs$1.readSync(fd, buf, 0, buf.length, 0);
426
+ return buf.subarray(0, n).toString("utf8").startsWith(LFS_POINTER_PREFIX);
427
+ } finally {
428
+ fs$1.closeSync(fd);
429
+ }
430
+ }
431
+ async function hashFileSha256(filePath) {
432
+ const hash = createHash("sha256");
433
+ await new Promise((resolve, reject) => {
434
+ fs$1.createReadStream(filePath).on("data", (chunk) => hash.update(chunk)).on("end", () => resolve()).on("error", reject);
435
+ });
436
+ return hash.digest("hex");
437
+ }
438
+ function assertInside(root, dest) {
439
+ const rootResolved = path$1.resolve(root);
440
+ const destResolved = path$1.resolve(dest);
441
+ const prefix = rootResolved.endsWith(path$1.sep) ? rootResolved : `${rootResolved}${path$1.sep}`;
442
+ if (destResolved !== rootResolved && !destResolved.startsWith(prefix)) throw new Error(`Refusing to write outside the destination root: ${dest}`);
443
+ return destResolved;
444
+ }
445
+ function rejectSymlink(filePath) {
446
+ try {
447
+ if (fs$1.lstatSync(filePath).isSymbolicLink()) throw new Error(`Refusing symlink at ${filePath}`);
448
+ } catch (err) {
449
+ if (err.code === "ENOENT") return;
450
+ throw err;
451
+ }
452
+ }
453
+ function sizeOutOfTolerance(actual, declared) {
454
+ if (declared <= 0) return true;
455
+ return Math.abs(actual - declared) / declared > SIZE_TOLERANCE;
456
+ }
457
+ function localBasename(entry) {
458
+ 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`;
459
+ if (entry.format === "openvino") return `${entry.id}.xml`;
460
+ if (entry.format === "tflite") return `${entry.id}.tflite`;
461
+ return entry.files[0]?.relativePath.split("/").pop() ?? `${entry.id}.onnx`;
462
+ }
463
+ function buildImportedDescriptor(entry, localName) {
464
+ const catalog = {
465
+ ...entry.model,
466
+ classMap: SCRYPTED_IDENTITY_CLASS_MAP,
467
+ formats: { ...entry.model.formats }
468
+ };
469
+ const format = entry.format;
470
+ const previous = catalog.formats[format];
471
+ if (format === "coreml") catalog.formats = {
472
+ ...catalog.formats,
473
+ coreml: {
474
+ url: `camstack-local://${entry.id}/${localName}`,
475
+ sizeMB: previous?.sizeMB ?? entry.files.reduce((sum, file) => sum + file.size, 0) / 1e6,
476
+ isDirectory: true,
477
+ files: previous?.files ?? [
478
+ "Manifest.json",
479
+ "Data/com.apple.CoreML/model.mlmodel",
480
+ "Data/com.apple.CoreML/weights/weight.bin"
481
+ ],
482
+ runtimes: ["python"]
483
+ }
484
+ };
485
+ else if (format === "openvino") catalog.formats = {
486
+ ...catalog.formats,
487
+ openvino: {
488
+ url: `camstack-local://${entry.id}/${localName}`,
489
+ sizeMB: previous?.sizeMB ?? entry.files.reduce((sum, file) => sum + file.size, 0) / 1e6,
490
+ files: [`${entry.id}.bin`],
491
+ runtimes: ["python"]
492
+ }
493
+ };
494
+ else catalog.formats = {
495
+ ...catalog.formats,
496
+ [format]: {
497
+ url: `camstack-local://${entry.id}/${localName}`,
498
+ sizeMB: previous?.sizeMB ?? entry.files.reduce((sum, file) => sum + file.size, 0) / 1e6,
499
+ ...previous?.runtimes !== void 0 ? { runtimes: previous.runtimes } : {}
500
+ }
501
+ };
502
+ return {
503
+ stepId: "object-detection",
504
+ entry: catalog
505
+ };
506
+ }
507
+ /** Availability/format selection uses the manifesto format, never key order. */
508
+ function resolveScryptedImportedFormat(manifestId, descriptor) {
509
+ const entry = getScryptedManifestEntry(manifestId);
510
+ if (!entry) throw new Error(`resolveScryptedImportedFormat: unknown manifest id "${manifestId}"`);
511
+ const format = entry.format;
512
+ if (!descriptor.entry.formats[format]) throw new Error(`resolveScryptedImportedFormat: descriptor for "${manifestId}" is missing manifesto format ${format}`);
513
+ return format;
514
+ }
515
+ async function verifyScryptedStagedFile(destPath, file, deps) {
516
+ const maxBytes = deps.maxFileBytes ?? 268435456;
517
+ if (file.size > maxBytes) throw new Error(`Declared size ${file.size} exceeds the ${maxBytes}-byte limit`);
518
+ if (!fs$1.existsSync(destPath)) throw new Error(`missing downloaded file for ${file.relativePath}`);
519
+ rejectSymlink(destPath);
520
+ const actual = fs$1.statSync(destPath).size;
521
+ if (actual > maxBytes) throw new Error(`Downloaded ${actual} bytes exceeds the ${maxBytes}-byte limit`);
522
+ if (isLfsPointer(destPath)) throw new Error(`Downloaded an LFS pointer instead of the artifact (${file.relativePath})`);
523
+ if (sizeOutOfTolerance(actual, file.size)) throw new Error(`Downloaded size ${actual} is outside tolerance of declared ${file.size} for ${file.relativePath}`);
524
+ const expected = assertScryptedFileSha256(file.sha256, file.relativePath);
525
+ if (await (deps.fileSha256 ?? hashFileSha256)(destPath) !== expected) throw new Error(`sha256 checksum mismatch for ${file.relativePath}`);
526
+ }
527
+ async function importScryptedCatalogModel(input, deps) {
528
+ const entry = (deps.resolveEntry ?? getScryptedManifestEntry)(input.manifestId);
529
+ if (!entry) throw new Error(`importScryptedCatalogModel: unknown manifest id "${input.manifestId}"`);
530
+ for (const file of entry.files) assertScryptedFileSha256(file.sha256, file.relativePath);
531
+ const drift = normalizeScryptedDrift(await deps.driftFor(entry.id));
532
+ if (drift.status !== "ok") throw new Error(`importScryptedCatalogModel: entry "${entry.id}" is blocked by drift (${drift.status}${drift.details ? `: ${drift.details}` : ""})`);
533
+ fs$1.mkdirSync(deps.stagingDir, { recursive: true });
534
+ fs$1.mkdirSync(deps.modelsDir, { recursive: true });
535
+ const stagingRoot = path$1.join(deps.stagingDir, `imp-${randomUUID()}`);
536
+ fs$1.mkdirSync(stagingRoot, { recursive: true });
537
+ try {
538
+ const staged = [];
539
+ for (const file of entry.files) {
540
+ const relative = assertScryptedRelativePath(file.relativePath);
541
+ const dest = assertInside(stagingRoot, path$1.join(stagingRoot, ...relative.split("/")));
542
+ const url = buildScryptedResolveUrl(relative);
543
+ await deps.downloadFile(url, dest);
544
+ await verifyScryptedStagedFile(dest, file, deps);
545
+ staged.push({
546
+ file,
547
+ dest
548
+ });
549
+ }
550
+ if (entry.format === "coreml") {
551
+ const hasManifest = staged.some((row) => row.file.relativePath.endsWith("Manifest.json"));
552
+ const hasModel = staged.some((row) => row.file.relativePath.endsWith("model.mlmodel"));
553
+ const hasWeight = staged.some((row) => row.file.relativePath.endsWith("weight.bin"));
554
+ if (!hasManifest || !hasModel || !hasWeight || staged.length < 3) throw new Error("CoreML payload is incomplete — Manifest.json, model and weights are required");
555
+ }
556
+ if (entry.format === "openvino") {
557
+ const hasXml = staged.some((row) => row.file.relativePath.endsWith(".xml"));
558
+ const hasBin = staged.some((row) => row.file.relativePath.endsWith(".bin"));
559
+ if (!hasXml || !hasBin) throw new Error("OpenVINO payload is incomplete — xml and bin siblings are required");
560
+ }
561
+ const localName = localBasename(entry);
562
+ const finalMain = assertInside(deps.modelsDir, path$1.join(deps.modelsDir, localName));
563
+ rejectSymlink(finalMain);
564
+ if (entry.format === "coreml") {
565
+ const pkgRel = staged.find((row) => row.file.relativePath.endsWith("Manifest.json"))?.file.relativePath.replace(/\/Manifest\.json$/, "");
566
+ if (!pkgRel) throw new Error("CoreML payload is incomplete — Manifest.json missing");
567
+ const stagedPkg = path$1.join(stagingRoot, ...pkgRel.split("/"));
568
+ fs$1.rmSync(finalMain, {
569
+ recursive: true,
570
+ force: true
571
+ });
572
+ fs$1.renameSync(stagedPkg, finalMain);
573
+ } else if (entry.format === "openvino") {
574
+ const xml = staged.find((row) => row.file.relativePath.endsWith(".xml"));
575
+ const bin = staged.find((row) => row.file.relativePath.endsWith(".bin"));
576
+ if (!xml || !bin) throw new Error("OpenVINO payload is incomplete");
577
+ const xmlDest = finalMain;
578
+ const binDest = assertInside(deps.modelsDir, path$1.join(deps.modelsDir, `${entry.id}.bin`));
579
+ fs$1.renameSync(xml.dest, xmlDest);
580
+ fs$1.renameSync(bin.dest, binDest);
581
+ } else {
582
+ const only = staged[0];
583
+ if (!only) throw new Error("payload is incomplete");
584
+ fs$1.renameSync(only.dest, finalMain);
585
+ }
586
+ return buildImportedDescriptor(entry, localName);
587
+ } finally {
588
+ fs$1.rmSync(stagingRoot, {
589
+ recursive: true,
590
+ force: true
591
+ });
592
+ }
593
+ }
594
+ //#endregion
70
595
  //#region ../types/dist/event-category-CIa_iT6b.mjs
71
596
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
72
597
  EventCategory["SystemBoot"] = "system.boot";
@@ -8131,6 +8656,15 @@ var LabelDefinitionSchema = object({
8131
8656
  description: string().optional(),
8132
8657
  icon: string().optional()
8133
8658
  });
8659
+ var ClassMapDefinitionSchema = object({
8660
+ mapping: record(string(), _enum([
8661
+ "person",
8662
+ "vehicle",
8663
+ "animal",
8664
+ "package"
8665
+ ])),
8666
+ preserveOriginal: boolean()
8667
+ });
8134
8668
  var MODEL_FORMATS = [
8135
8669
  "onnx",
8136
8670
  "coreml",
@@ -8309,7 +8843,13 @@ var ModelCatalogEntrySchema = object({
8309
8843
  * `id` stays the source of truth for resolution/download/persistence; grouping
8310
8844
  * is a presentation overlay resolved back to an `id`.
8311
8845
  */
8312
- group: ModelVariantGroupSchema.optional()
8846
+ group: ModelVariantGroupSchema.optional(),
8847
+ /**
8848
+ * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8849
+ * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8850
+ * labels already ARE the CamStack macros (Scrypted identity map).
8851
+ */
8852
+ classMap: ClassMapDefinitionSchema.optional()
8313
8853
  });
8314
8854
  var ConvertTargetSchema = discriminatedUnion("format", [object({
8315
8855
  format: literal("openvino"),
@@ -8338,7 +8878,8 @@ var ModelConvertMetadataSchema = object({
8338
8878
  "ocr",
8339
8879
  "segmentation"
8340
8880
  ]),
8341
- faceAlignment: boolean().optional()
8881
+ faceAlignment: boolean().optional(),
8882
+ classMap: ClassMapDefinitionSchema.optional()
8342
8883
  });
8343
8884
  var ConvertArtifactSchema = object({
8344
8885
  format: _enum(MODEL_FORMATS),
@@ -14518,12 +15059,15 @@ var NcOccupancyConditionSchema = object({
14518
15059
  * there is no second switch that can disagree with the first and every rule
14519
15060
  * authored before the decision migrates for free (`audioModeOf`):
14520
15061
  *
14521
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14522
- * classifier labels with one of them. No window, no percentage:
14523
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14524
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14525
- * the analyzer's (`classificationMinScore`, per device) a label only
14526
- * reaches this condition if the classifier was already confident enough.
15062
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
15063
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
15064
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
15065
+ * frames is the wrong question for a classifier that labels 1–3 frames
15066
+ * per episode. The count window is the brake that drops a single-frame
15067
+ * false positive; the rule's own `throttle` cooldown is the other. The
15068
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
15069
+ * per device) — a label only reaches this condition if the classifier was
15070
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14527
15071
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14528
15072
  * the condition: at least `hitPercent`% of the samples over
14529
15073
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14550,14 +15094,22 @@ var NcOccupancyConditionSchema = object({
14550
15094
  * an operator who typed `dog` mean the same thing.
14551
15095
  */
14552
15096
  var NcAudioConditionSchema = object({
14553
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
15097
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14554
15098
  labels: array(string().min(1)).min(1).optional(),
14555
15099
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14556
15100
  dbThreshold: number().min(-96).max(0).optional(),
14557
15101
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14558
15102
  hitPercent: number().int().min(1).max(100).default(60),
14559
15103
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14560
- samplingSeconds: number().int().min(1).max(300).default(10)
15104
+ samplingSeconds: number().int().min(1).max(300).default(10),
15105
+ /**
15106
+ * LABEL MODE: how many labelled frames must land inside
15107
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
15108
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
15109
+ */
15110
+ confirmHits: number().int().min(1).max(20).optional(),
15111
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
15112
+ confirmWindowSec: number().int().min(1).max(60).optional()
14561
15113
  });
14562
15114
  /**
14563
15115
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -17006,7 +17558,9 @@ var TrackCascadeCountsSchema = object({
17006
17558
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17007
17559
  plates: number().int(),
17008
17560
  /** Per-track CLIP search vectors removed (best-effort). */
17009
- embeddings: number().int()
17561
+ embeddings: number().int(),
17562
+ /** Group membership + group rows removed with their last member (best-effort). */
17563
+ groups: number().int()
17010
17564
  });
17011
17565
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17012
17566
  var DiskReconcileCountsSchema = object({
@@ -17370,6 +17924,33 @@ var NativeCropRefSchema = object({
17370
17924
  h: number()
17371
17925
  })
17372
17926
  });
17927
+ object({
17928
+ crop: object({
17929
+ left: number(),
17930
+ top: number(),
17931
+ width: number().positive(),
17932
+ height: number().positive()
17933
+ }).optional(),
17934
+ content: object({
17935
+ width: number().int().positive(),
17936
+ height: number().int().positive()
17937
+ }),
17938
+ fit: _enum(["stretch", "contain"]),
17939
+ format: _enum([
17940
+ "rgb",
17941
+ "gray",
17942
+ "jpeg"
17943
+ ])
17944
+ });
17945
+ var FrameRefSchema = object({
17946
+ registryId: string().min(1),
17947
+ id: string().min(1),
17948
+ width: number().int().positive(),
17949
+ height: number().int().positive(),
17950
+ format: _enum(["rgb", "gray"]),
17951
+ timestamp: number(),
17952
+ capturedAt: number().optional()
17953
+ });
17373
17954
  var ModelFormatSchema$1 = _enum([
17374
17955
  "onnx",
17375
17956
  "coreml",
@@ -17614,6 +18195,12 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
17614
18195
  steps: array(PipelineStepInputSchema).min(1),
17615
18196
  frame: FrameInputSchema.optional(),
17616
18197
  /**
18198
+ * Process-local lazy frame. Valid only when caller and provider resolve
18199
+ * in the same execution-group process; split/cross-node callers use
18200
+ * `frame`/`image` inline compatibility instead.
18201
+ */
18202
+ frameRef: FrameRefSchema.optional(),
18203
+ /**
17617
18204
  * CB5 shm passthrough — a `FrameHandle` naming the same ring slot
17618
18205
  * the decoded pixels live in. One more member of the one-of
17619
18206
  * frame/frameHandle/image/imageBase64/referenceImage group.
@@ -17869,7 +18456,10 @@ var NativeCropResultSchema = object({
17869
18456
  * Which source served this crop, so a quality-sensitive consumer (the native
17870
18457
  * `keyFrame`) can reject a degraded fallback:
17871
18458
  * - `native` — cut from the decode worker's retained NATIVE surface (the
17872
- * quality path).
18459
+ * quality path). A subject-tile serve is also native-resolution and stays
18460
+ * `native` here: the public enum cannot name `tile` without a breaking cap
18461
+ * change. Runner telemetry distinguishes lease vs tile via `source` on the
18462
+ * internal crop result (`nativeHits` vs `tileHits`).
17873
18463
  * - `ram-fullframe` — the native surface MISSED but the request was full-frame,
17874
18464
  * so the ≤640 RAM `RetainedFrameStore` served it (honest lower-res; legit for
17875
18465
  * the blank-frame guard / 640-snapshot resolve, NOT for the clean keyFrame).
@@ -18360,12 +18950,41 @@ var RunnerLocalLoadSchema = object({
18360
18950
  * legacy `OrchestratorMetricsSchema` shape so existing dashboards keep
18361
18951
  * working unchanged when they switch to reading from the runner cap.
18362
18952
  */
18953
+ var FrameLazyCountersSchema = object({
18954
+ framesDecoded: number(),
18955
+ framesAdmitted: number(),
18956
+ framesDroppedPixelFree: number(),
18957
+ viewsMaterialized: number(),
18958
+ viewsSkipped: number(),
18959
+ workerToRunnerBytes: number(),
18960
+ runnerToPoolRawBytes: number(),
18961
+ runnerToPoolJpegBytes: number(),
18962
+ onDemandFullFrameRequests: number(),
18963
+ onDemandCropRequests: number(),
18964
+ nativeHits: number(),
18965
+ nativeMisses: number(),
18966
+ tileHits: number(),
18967
+ tileMisses: number(),
18968
+ fallbackHits: number(),
18969
+ fallbackMisses: number(),
18970
+ retainedWritesAvoided: number(),
18971
+ residentRefs: number(),
18972
+ residentBytes: number(),
18973
+ releases: number(),
18974
+ evictions: number(),
18975
+ staleMisses: number()
18976
+ });
18977
+ var FrameLazyMetricsSchema = object({
18978
+ node: FrameLazyCountersSchema,
18979
+ cameras: array(FrameLazyCountersSchema.extend({ deviceId: number() }))
18980
+ });
18363
18981
  var RunnerLocalMetricsSchema = object({
18364
18982
  nodeId: string(),
18365
18983
  activeCameras: number(),
18366
18984
  throttledCameras: number(),
18367
18985
  avgInferenceTimeMs: number(),
18368
- queueDepth: number()
18986
+ queueDepth: number(),
18987
+ frameLazy: FrameLazyMetricsSchema.optional()
18369
18988
  });
18370
18989
  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({
18371
18990
  handle: FrameHandleSchema,
@@ -19665,6 +20284,9 @@ method(_void(), ProviderInfoSchema), method(object({ config: record(string(), un
19665
20284
  location: StorageLocationSchema,
19666
20285
  relativePath: string()
19667
20286
  }), _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" });
20287
+ /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
20288
+ var ProfileSettingsSchemaBridge = unknown().nullable();
20289
+ var ProfileSettingsBagSchema = record(string(), unknown());
19668
20290
  /**
19669
20291
  * A live terminal session hosted by the provider addon. Output and input do
19670
20292
  * NOT flow through the capability — they use the addon data plane
@@ -19694,7 +20316,14 @@ var TerminalSessionInfoSchema = object({
19694
20316
  var TerminalProfileInfoSchema = object({
19695
20317
  profileId: string(),
19696
20318
  label: string(),
19697
- description: string().optional()
20319
+ description: string().optional(),
20320
+ /** Spawn defaults the instance form copies on create. */
20321
+ executable: string().optional(),
20322
+ args: array(string()).readonly().optional(),
20323
+ cwd: string().optional(),
20324
+ environment: array(string()).readonly().optional(),
20325
+ /** ConfigUISchema for instance knobs, or null when the profile has none. */
20326
+ settingsSchema: ProfileSettingsSchemaBridge.optional()
19698
20327
  });
19699
20328
  /**
19700
20329
  * A durable operator-created Terminal instance. Profiles are templates; only
@@ -19707,7 +20336,12 @@ var TerminalInstanceInfoSchema = object({
19707
20336
  profileId: string(),
19708
20337
  profileLabel: string(),
19709
20338
  name: string(),
19710
- enabled: boolean()
20339
+ enabled: boolean(),
20340
+ executable: string(),
20341
+ args: array(string()).readonly(),
20342
+ cwd: string(),
20343
+ environment: array(string()).readonly(),
20344
+ profileSettings: ProfileSettingsBagSchema
19711
20345
  });
19712
20346
  var TerminalLegacyCameraSchema = object({
19713
20347
  stableId: string(),
@@ -19737,7 +20371,23 @@ var TerminalOutputBatchSchema = object({
19737
20371
  method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
19738
20372
  targetNodeId: string().min(1),
19739
20373
  profileId: string().min(1),
19740
- name: string().trim().min(1).max(160).optional()
20374
+ name: string().trim().min(1).max(160).optional(),
20375
+ executable: string().max(1024).optional(),
20376
+ args: array(string().max(2048)).max(64).optional(),
20377
+ cwd: string().max(1024).optional(),
20378
+ environment: array(string().max(4096)).max(64).optional(),
20379
+ profileSettings: ProfileSettingsBagSchema.optional()
20380
+ }), TerminalInstanceInfoSchema, {
20381
+ kind: "mutation",
20382
+ auth: "admin"
20383
+ }), method(object({
20384
+ instanceId: string().min(1),
20385
+ name: string().trim().min(1).max(160).optional(),
20386
+ executable: string().max(1024).optional(),
20387
+ args: array(string().max(2048)).max(64).optional(),
20388
+ cwd: string().max(1024).optional(),
20389
+ environment: array(string().max(4096)).max(64).optional(),
20390
+ profileSettings: ProfileSettingsBagSchema.optional()
19741
20391
  }), TerminalInstanceInfoSchema, {
19742
20392
  kind: "mutation",
19743
20393
  auth: "admin"
@@ -19759,7 +20409,11 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
19759
20409
  }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
19760
20410
  profileId: string(),
19761
20411
  cols: number().int().positive(),
19762
- rows: number().int().positive()
20412
+ rows: number().int().positive(),
20413
+ executable: string().max(1024).optional(),
20414
+ args: array(string().max(2048)).max(64).optional(),
20415
+ cwd: string().max(1024).optional(),
20416
+ environment: array(string().max(4096)).max(64).optional()
19763
20417
  }), TerminalSessionInfoSchema, {
19764
20418
  kind: "mutation",
19765
20419
  auth: "admin"
@@ -22541,10 +23195,10 @@ DeviceType.LawnMower, method(object({ deviceId: number().int().nonnegative() }),
22541
23195
  *
22542
23196
  * • The SDK (mobile / web client) consumes `getConnectionEndpoints()`
22543
23197
  * to receive an ordered list of candidate base URLs it should race
22544
- * on connect — LAN IPv4 first (lowest latency when on same network),
22545
- * then public hostname (if a tunnel is up), then IPv6. The SDK can
22546
- * race them with short timeouts and stick with the winner for the
22547
- * session.
23198
+ * on connect — LAN IPv4 and stable LAN IPv6 first (lowest latency
23199
+ * when on the same network), then public hostname (if a tunnel is
23200
+ * up). The SDK can race them with short timeouts and stick with the
23201
+ * winner for the session.
22548
23202
  *
22549
23203
  * Why hub-only: agents are not directly addressable by the operator's
22550
23204
  * clients — they reverse-connect to the hub. Exposing their interfaces
@@ -22699,6 +23353,17 @@ var NotificationEndpointSchema = object({
22699
23353
  /** What the ranking currently resolves to (null when nothing is reachable). */
22700
23354
  resolved: string().nullable()
22701
23355
  });
23356
+ /**
23357
+ * The URLs the SDK / viewer should race for API access. `baseUrls` empty =
23358
+ * AUTO (every LAN IPv4 + the public tunnel). `resolved` is what that choice
23359
+ * currently expands to, so the UI can show the effective set either way.
23360
+ */
23361
+ var ViewerEndpointsSchema = object({
23362
+ /** The operator's explicit race set, or empty for AUTO. */
23363
+ baseUrls: array(string()).readonly(),
23364
+ /** What the ranking currently resolves to (may be empty if nothing is up). */
23365
+ resolved: array(string()).readonly()
23366
+ });
22702
23367
  var AllowedAddressesSchema = object({
22703
23368
  /**
22704
23369
  * Allowlist of interface addresses operators have explicitly opted
@@ -22707,6 +23372,20 @@ var AllowedAddressesSchema = object({
22707
23372
  * Network Addresses admin page and persisted by the addon.
22708
23373
  */
22709
23374
  addresses: array(string()).readonly() });
23375
+ var TlsStatusSchema = object({
23376
+ mode: _enum([
23377
+ "generated",
23378
+ "uploaded",
23379
+ "disabled"
23380
+ ]),
23381
+ leafFingerprintSha256: string().nullable(),
23382
+ caFingerprintSha256: string().nullable(),
23383
+ validTo: string().nullable(),
23384
+ sans: array(string()),
23385
+ caCertPem: string().nullable(),
23386
+ reissueError: string().nullable(),
23387
+ restartRequired: boolean()
23388
+ });
22710
23389
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
22711
23390
  /**
22712
23391
  * LEGACY HINT — do not send from new code. Kept optional so clients
@@ -22716,17 +23395,31 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
22716
23395
  */
22717
23396
  port: number().int().min(1).max(65535).optional(),
22718
23397
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
22719
- * candidate. Default `true`. */
23398
+ * candidate. Default `false` — loopback is not a client route. */
22720
23399
  includeLoopback: boolean().optional(),
22721
- /** Skip IPv6 entries. Some legacy clients can't parse them.
22722
- * Default `false`. */
23400
+ /** Skip IPv6 entries. Default `false` the palette includes stable
23401
+ * LAN IPv6 (ICE/WebRTC uses dual-stack regardless). Pass `true` to
23402
+ * hide them. The viewer HTTP/WS race is `getViewerEndpoints`. */
22723
23403
  ipv4Only: boolean().optional(),
22724
23404
  /** Scheme to emit for LAN/loopback URLs. Default `'http'`.
22725
23405
  * Pass `'https'` when the caller is itself loaded over HTTPS
22726
23406
  * to avoid mixed-content blocks in the browser. The public
22727
23407
  * tunnel always emits `https://` regardless. */
22728
23408
  scheme: _enum(["http", "https"]).optional()
22729
- }), 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" });
23409
+ }), 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, {
23410
+ kind: "mutation",
23411
+ auth: "admin"
23412
+ }), method(object({
23413
+ certPem: string().min(1),
23414
+ keyPem: string().min(1),
23415
+ caPem: string().optional()
23416
+ }), TlsStatusSchema, {
23417
+ kind: "mutation",
23418
+ auth: "admin"
23419
+ }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
23420
+ kind: "mutation",
23421
+ auth: "admin"
23422
+ });
22730
23423
  object({
22731
23424
  /** Lifecycle state of the lock. `jammed` means the motor reported
22732
23425
  * failure to reach the target — operator intervention required. */
@@ -28489,6 +29182,12 @@ Object.freeze({
28489
29182
  addonId: null,
28490
29183
  access: "create"
28491
29184
  },
29185
+ "localNetwork.downloadCa": {
29186
+ capName: "local-network",
29187
+ capScope: "system",
29188
+ addonId: null,
29189
+ access: "view"
29190
+ },
28492
29191
  "localNetwork.getAllowedAddresses": {
28493
29192
  capName: "local-network",
28494
29193
  capScope: "system",
@@ -28513,18 +29212,42 @@ Object.freeze({
28513
29212
  addonId: null,
28514
29213
  access: "view"
28515
29214
  },
29215
+ "localNetwork.getTlsStatus": {
29216
+ capName: "local-network",
29217
+ capScope: "system",
29218
+ addonId: null,
29219
+ access: "view"
29220
+ },
29221
+ "localNetwork.getViewerEndpoints": {
29222
+ capName: "local-network",
29223
+ capScope: "system",
29224
+ addonId: null,
29225
+ access: "view"
29226
+ },
28516
29227
  "localNetwork.list": {
28517
29228
  capName: "local-network",
28518
29229
  capScope: "system",
28519
29230
  addonId: null,
28520
29231
  access: "view"
28521
29232
  },
29233
+ "localNetwork.regenerateCertificate": {
29234
+ capName: "local-network",
29235
+ capScope: "system",
29236
+ addonId: null,
29237
+ access: "create"
29238
+ },
28522
29239
  "localNetwork.resetAllowlistToBestMatch": {
28523
29240
  capName: "local-network",
28524
29241
  capScope: "system",
28525
29242
  addonId: null,
28526
29243
  access: "delete"
28527
29244
  },
29245
+ "localNetwork.revertToGeneratedCertificate": {
29246
+ capName: "local-network",
29247
+ capScope: "system",
29248
+ addonId: null,
29249
+ access: "create"
29250
+ },
28528
29251
  "localNetwork.setAllowedAddresses": {
28529
29252
  capName: "local-network",
28530
29253
  capScope: "system",
@@ -28537,6 +29260,18 @@ Object.freeze({
28537
29260
  addonId: null,
28538
29261
  access: "create"
28539
29262
  },
29263
+ "localNetwork.setViewerEndpoints": {
29264
+ capName: "local-network",
29265
+ capScope: "system",
29266
+ addonId: null,
29267
+ access: "create"
29268
+ },
29269
+ "localNetwork.uploadCertificate": {
29270
+ capName: "local-network",
29271
+ capScope: "system",
29272
+ addonId: null,
29273
+ access: "create"
29274
+ },
28540
29275
  "lockControl.lock": {
28541
29276
  capName: "lock-control",
28542
29277
  capScope: "device",
@@ -31417,6 +32152,12 @@ Object.freeze({
31417
32152
  addonId: null,
31418
32153
  access: "create"
31419
32154
  },
32155
+ "terminalSession.updateInstance": {
32156
+ capName: "terminal-session",
32157
+ capScope: "system",
32158
+ addonId: null,
32159
+ access: "create"
32160
+ },
31420
32161
  "terminalSession.writeInput": {
31421
32162
  capName: "terminal-session",
31422
32163
  capScope: "system",
@@ -33904,6 +34645,35 @@ Object.freeze(Object.fromEntries([{
33904
34645
  }]
33905
34646
  }].map((s) => [s.stepId, s.defaultModelId])));
33906
34647
  string().min(1);
34648
+ var CLUSTER_STEP_SETTING_FIELDS = [{
34649
+ stepId: "face-embedding",
34650
+ key: "minLandmarkFaceSize",
34651
+ label: "Min face size for recognition (detection px)",
34652
+ 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.",
34653
+ type: "slider",
34654
+ min: 0,
34655
+ max: 64,
34656
+ step: 2,
34657
+ default: 24
34658
+ }];
34659
+ function clusterStepSettingKey(stepId, fieldKey) {
34660
+ return `clusterStepSetting:${stepId}:${fieldKey}`;
34661
+ }
34662
+ var ClusterSettingNumberSchema = number().finite();
34663
+ function readClusterStepSettings(config) {
34664
+ const out = {};
34665
+ for (const field of CLUSTER_STEP_SETTING_FIELDS) {
34666
+ const parsed = ClusterSettingNumberSchema.safeParse(config[clusterStepSettingKey(field.stepId, field.key)]);
34667
+ const value = parsed.success ? parsed.data : field.default;
34668
+ const existing = out[field.stepId] ?? {};
34669
+ out[field.stepId] = {
34670
+ ...existing,
34671
+ [field.key]: value
34672
+ };
34673
+ }
34674
+ return out;
34675
+ }
34676
+ readClusterStepSettings({});
33907
34677
  object({
33908
34678
  /**
33909
34679
  * Fraction of the box's own size added on EACH side before cutting.
@@ -34153,7 +34923,8 @@ function buildEntry(metadata, artifacts) {
34153
34923
  ...metadata.inputLayout !== void 0 ? { inputLayout: metadata.inputLayout } : {},
34154
34924
  ...metadata.inputNormalization !== void 0 ? { inputNormalization: metadata.inputNormalization } : {},
34155
34925
  ...metadata.preprocessMode !== void 0 ? { preprocessMode: metadata.preprocessMode } : {},
34156
- ...metadata.faceAlignment !== void 0 ? { faceAlignment: metadata.faceAlignment } : {}
34926
+ ...metadata.faceAlignment !== void 0 ? { faceAlignment: metadata.faceAlignment } : {},
34927
+ ...metadata.classMap !== void 0 ? { classMap: metadata.classMap } : {}
34157
34928
  };
34158
34929
  }
34159
34930
  function buildModelConvertProvider(deps) {
@@ -34690,6 +35461,45 @@ var ModelNodeAvailabilitySchema = object({
34690
35461
  });
34691
35462
  /** `modelId → nodeId → availability`. */
34692
35463
  var ModelAvailabilityMapSchema = record(string(), record(string(), ModelNodeAvailabilitySchema));
35464
+ var ScryptedManifestFileSchema = object({
35465
+ relativePath: string(),
35466
+ size: number(),
35467
+ sha256: string().regex(/^[0-9a-f]{64}$/)
35468
+ });
35469
+ var ScryptedManifestEntrySchema = object({
35470
+ id: string(),
35471
+ label: string(),
35472
+ family: _enum([
35473
+ "yolov9t",
35474
+ "yolov9s",
35475
+ "yolov9m",
35476
+ "yolov9c"
35477
+ ]),
35478
+ variant: _enum(["relu", "relu_test"]),
35479
+ format: _enum([
35480
+ "coreml",
35481
+ "onnx",
35482
+ "openvino",
35483
+ "tflite"
35484
+ ]),
35485
+ precision: string(),
35486
+ inputSize: object({
35487
+ width: number(),
35488
+ height: number()
35489
+ }),
35490
+ files: array(ScryptedManifestFileSchema).readonly(),
35491
+ model: ModelCatalogEntrySchema
35492
+ });
35493
+ var ScryptedCatalogDriftSchema = object({
35494
+ manifestId: string(),
35495
+ status: _enum([
35496
+ "ok",
35497
+ "missing",
35498
+ "size-changed",
35499
+ "unavailable"
35500
+ ]),
35501
+ details: string().optional()
35502
+ });
34693
35503
  var modelStudioActions = defineCustomActions({
34694
35504
  registerCustomModel: customAction(CustomModelDescriptorSchema, object({ ok: literal(true) }), { kind: "mutation" }),
34695
35505
  removeCustomModel: customAction(object({ modelId: string() }), object({ removed: boolean() }), { kind: "mutation" }),
@@ -34756,6 +35566,16 @@ var modelStudioActions = defineCustomActions({
34756
35566
  bytes: number()
34757
35567
  }), { kind: "mutation" }),
34758
35568
  /**
35569
+ * Local Scrypted YOLO catalog (Task 7B). Listing is a projection of the
35570
+ * revision-pinned manifesto plus a read-only HF drift check. Import accepts
35571
+ * ONLY `manifestId` — host/repo/revision/path/labels/checksum stay server-owned.
35572
+ */
35573
+ listScryptedCatalog: customAction(_void(), object({
35574
+ entries: array(ScryptedManifestEntrySchema).readonly(),
35575
+ drift: array(ScryptedCatalogDriftSchema).readonly()
35576
+ })),
35577
+ importScryptedCatalogModel: customAction(object({ manifestId: string() }), CustomModelDescriptorSchema, { kind: "mutation" }),
35578
+ /**
34759
35579
  * Frigate+ personal models. The API key lives in the addon's SETTINGS
34760
35580
  * (password field) and never leaves the hub: list / test / import all run
34761
35581
  * server-side against api.frigate.video. Status deliberately reveals only
@@ -35020,6 +35840,8 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
35020
35840
  notes: c.notes
35021
35841
  })),
35022
35842
  importFrigateCatalogModel: async (input) => this.importFrigateCatalogModel(input),
35843
+ listScryptedCatalog: async () => this.listScryptedCatalog(),
35844
+ importScryptedCatalogModel: async (input) => this.importScryptedCatalogFromManifest(input),
35023
35845
  tfliteSupport: async () => {
35024
35846
  const api = this.ctx.api;
35025
35847
  if (!api) return { nodes: [] };
@@ -35212,6 +36034,78 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
35212
36034
  return hash.digest("hex");
35213
36035
  }
35214
36036
  /**
36037
+ * Projection of the local Scrypted YOLO manifesto plus a read-only HF drift
36038
+ * check. Network errors become `unavailable` rows — the manifesto is never
36039
+ * mutated and nothing is auto-imported.
36040
+ */
36041
+ async listScryptedCatalog() {
36042
+ try {
36043
+ return {
36044
+ entries: SCRYPTED_YOLO_MANIFEST,
36045
+ drift: compareScryptedManifestDrift(SCRYPTED_YOLO_MANIFEST, await fetchScryptedRemoteTree({ fetchImpl: fetch })).map(normalizeScryptedDrift)
36046
+ };
36047
+ } catch (err) {
36048
+ const details = err instanceof Error ? err.message : "Hugging Face tree unavailable";
36049
+ return {
36050
+ entries: SCRYPTED_YOLO_MANIFEST,
36051
+ drift: SCRYPTED_YOLO_MANIFEST.map((entry) => ({
36052
+ manifestId: entry.id,
36053
+ status: "unavailable",
36054
+ details
36055
+ }))
36056
+ };
36057
+ }
36058
+ }
36059
+ /**
36060
+ * Import one manifesto entry by id. Host/repo/revision/path stay server-owned.
36061
+ */
36062
+ async importScryptedCatalogFromManifest(input) {
36063
+ const modelsDir = await this.resolveModelsDir();
36064
+ const descriptor = await importScryptedCatalogModel(input, {
36065
+ stagingDir: path.join(this.ctx.dataDir ?? os.tmpdir(), "scrypted-import"),
36066
+ modelsDir,
36067
+ downloadFile: (url, destPath) => downloadFile(url, destPath, {
36068
+ redirectPolicy: assertScryptedDownloadUrl,
36069
+ maxBytes: SCRYPTED_IMPORT_MAX_FILE_BYTES,
36070
+ timeoutMs: 6e4
36071
+ }),
36072
+ driftFor: async (manifestId) => {
36073
+ try {
36074
+ const tree = await fetchScryptedRemoteTree({ fetchImpl: fetch });
36075
+ const row = compareScryptedManifestDrift(SCRYPTED_YOLO_MANIFEST.filter((entry) => entry.id === manifestId), tree)[0];
36076
+ return normalizeScryptedDrift(row ?? {
36077
+ manifestId,
36078
+ status: "missing",
36079
+ details: "entry missing from manifesto"
36080
+ });
36081
+ } catch (err) {
36082
+ return {
36083
+ manifestId,
36084
+ status: "unavailable",
36085
+ details: err instanceof Error ? err.message : "Hugging Face tree unavailable"
36086
+ };
36087
+ }
36088
+ }
36089
+ });
36090
+ await this.registerCustomModel(descriptor);
36091
+ const format = resolveScryptedImportedFormat(input.manifestId, descriptor);
36092
+ const manifestEntry = getScryptedManifestEntry(input.manifestId);
36093
+ if (!manifestEntry) throw new Error(`Imported Scrypted catalog model is missing manifesto entry ${input.manifestId}`);
36094
+ const bytes = Math.round((descriptor.entry.formats[format]?.sizeMB ?? 0) * 1e6);
36095
+ await this.availability_.update((prev) => upsertAvailability(prev, descriptor.entry.id, "hub", {
36096
+ format,
36097
+ sha256: scryptedBundleFingerprint(manifestEntry),
36098
+ bytes,
36099
+ at: Date.now()
36100
+ }));
36101
+ this.ctx.logger.info("Imported Scrypted catalog model", { meta: {
36102
+ manifestId: input.manifestId,
36103
+ modelId: descriptor.entry.id,
36104
+ bytes
36105
+ } });
36106
+ return descriptor;
36107
+ }
36108
+ /**
35215
36109
  * Import a curated PUBLIC Frigate catalog model: download SERVER-SIDE into
35216
36110
  * the hub's modelsDir (size-verified against the curated declaration),
35217
36111
  * register the descriptor with the PUBLIC url (nodes download on demand),