@camstack/addon-model-studio 1.1.33 → 1.1.35

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-c5QftjJr.mjs} +2 -2
  2. package/dist/{PrivacyMaskSettings-CtKzQjYD.mjs → PrivacyMaskSettings-D7-a5-3N.mjs} +4 -4
  3. package/dist/{SceneMonitorEditor-CM3z_N4Q.mjs → SceneMonitorEditor-CKP5eVgy.mjs} +3 -3
  4. package/dist/_stub.js +1740 -1018
  5. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-B0we2PiB.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-DA1xoR1n.mjs} +4 -4
  6. package/dist/_virtual_mf___mfe_internal__addon_model_studio_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DlVrDcQA.mjs +26 -0
  7. package/dist/addon-model-studio.css +1 -1
  8. package/dist/{hostInit-B-Vb5-CN.mjs → hostInit-DnVgXgR_.mjs} +3 -3
  9. package/dist/model-studio.addon.js +922 -32
  10. package/dist/model-studio.addon.mjs +922 -32
  11. package/dist/{player-overlays-CNkUzVZI.mjs → player-overlays-DQA3KJhP.mjs} +1 -1
  12. package/dist/remoteEntry.js +1 -1
  13. package/dist/{responsive-D-JIMJTX.mjs → responsive-B4KHcYzf.mjs} +1 -1
  14. package/dist/{square-CBwtzgNH.mjs → square-9pQrLRgw.mjs} +1 -1
  15. package/dist/{trash-2-CiJaurwE.mjs → trash-2-DRIA_3Ys.mjs} +1 -1
  16. package/dist/{use-device-snapshot-DpcJhtJX.mjs → use-device-snapshot-BTteNbQy.mjs} +1 -1
  17. package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-vDbogekh.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-BMAdzl3K.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,7 +126,496 @@ 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
93
- //#region ../types/dist/event-category-XfKNtfCc.mjs
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
618
+ //#region ../types/dist/event-category-CIa_iT6b.mjs
94
619
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
95
620
  EventCategory["SystemBoot"] = "system.boot";
96
621
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -106,6 +631,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
106
631
  */
107
632
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
108
633
  /**
634
+ * The hub reissued its own TLS certificate at boot (`ensureTlsCert`).
635
+ * Emitted only when the material on disk actually changed, so an
636
+ * operator who trusted the old certificate by hand is told rather than
637
+ * discovering it as a browser error. Payload `TlsCertChangedPayload`.
638
+ *
639
+ * Rule: docs/decisions/adr-0227-*.md
640
+ */
641
+ EventCategory["SystemTlsCertChanged"] = "system.tls-cert-changed";
642
+ /**
109
643
  * A newer addon or server-root package version was found by the
110
644
  * authoritative registry check. Emitted once when any observed
111
645
  * `latestVersion` changes (or a package/node first appears behind);
@@ -8145,6 +8679,15 @@ var LabelDefinitionSchema = object({
8145
8679
  description: string().optional(),
8146
8680
  icon: string().optional()
8147
8681
  });
8682
+ var ClassMapDefinitionSchema = object({
8683
+ mapping: record(string(), _enum([
8684
+ "person",
8685
+ "vehicle",
8686
+ "animal",
8687
+ "package"
8688
+ ])),
8689
+ preserveOriginal: boolean()
8690
+ });
8148
8691
  var MODEL_FORMATS = [
8149
8692
  "onnx",
8150
8693
  "coreml",
@@ -8323,7 +8866,13 @@ var ModelCatalogEntrySchema = object({
8323
8866
  * `id` stays the source of truth for resolution/download/persistence; grouping
8324
8867
  * is a presentation overlay resolved back to an `id`.
8325
8868
  */
8326
- 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()
8327
8876
  });
8328
8877
  var ConvertTargetSchema = discriminatedUnion("format", [object({
8329
8878
  format: literal("openvino"),
@@ -8352,7 +8901,8 @@ var ModelConvertMetadataSchema = object({
8352
8901
  "ocr",
8353
8902
  "segmentation"
8354
8903
  ]),
8355
- faceAlignment: boolean().optional()
8904
+ faceAlignment: boolean().optional(),
8905
+ classMap: ClassMapDefinitionSchema.optional()
8356
8906
  });
8357
8907
  var ConvertArtifactSchema = object({
8358
8908
  format: _enum(MODEL_FORMATS),
@@ -17384,6 +17934,33 @@ var NativeCropRefSchema = object({
17384
17934
  h: number()
17385
17935
  })
17386
17936
  });
17937
+ object({
17938
+ crop: object({
17939
+ left: number(),
17940
+ top: number(),
17941
+ width: number().positive(),
17942
+ height: number().positive()
17943
+ }).optional(),
17944
+ content: object({
17945
+ width: number().int().positive(),
17946
+ height: number().int().positive()
17947
+ }),
17948
+ fit: _enum(["stretch", "contain"]),
17949
+ format: _enum([
17950
+ "rgb",
17951
+ "gray",
17952
+ "jpeg"
17953
+ ])
17954
+ });
17955
+ var FrameRefSchema = object({
17956
+ registryId: string().min(1),
17957
+ id: string().min(1),
17958
+ width: number().int().positive(),
17959
+ height: number().int().positive(),
17960
+ format: _enum(["rgb", "gray"]),
17961
+ timestamp: number(),
17962
+ capturedAt: number().optional()
17963
+ });
17387
17964
  var ModelFormatSchema$1 = _enum([
17388
17965
  "onnx",
17389
17966
  "coreml",
@@ -17628,6 +18205,12 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
17628
18205
  steps: array(PipelineStepInputSchema).min(1),
17629
18206
  frame: FrameInputSchema.optional(),
17630
18207
  /**
18208
+ * Process-local lazy frame. Valid only when caller and provider resolve
18209
+ * in the same execution-group process; split/cross-node callers use
18210
+ * `frame`/`image` inline compatibility instead.
18211
+ */
18212
+ frameRef: FrameRefSchema.optional(),
18213
+ /**
17631
18214
  * CB5 shm passthrough — a `FrameHandle` naming the same ring slot
17632
18215
  * the decoded pixels live in. One more member of the one-of
17633
18216
  * frame/frameHandle/image/imageBase64/referenceImage group.
@@ -17883,7 +18466,10 @@ var NativeCropResultSchema = object({
17883
18466
  * Which source served this crop, so a quality-sensitive consumer (the native
17884
18467
  * `keyFrame`) can reject a degraded fallback:
17885
18468
  * - `native` — cut from the decode worker's retained NATIVE surface (the
17886
- * quality path).
18469
+ * quality path). A subject-tile serve is also native-resolution and stays
18470
+ * `native` here: the public enum cannot name `tile` without a breaking cap
18471
+ * change. Runner telemetry distinguishes lease vs tile via `source` on the
18472
+ * internal crop result (`nativeHits` vs `tileHits`).
17887
18473
  * - `ram-fullframe` — the native surface MISSED but the request was full-frame,
17888
18474
  * so the ≤640 RAM `RetainedFrameStore` served it (honest lower-res; legit for
17889
18475
  * the blank-frame guard / 640-snapshot resolve, NOT for the clean keyFrame).
@@ -18374,12 +18960,41 @@ var RunnerLocalLoadSchema = object({
18374
18960
  * legacy `OrchestratorMetricsSchema` shape so existing dashboards keep
18375
18961
  * working unchanged when they switch to reading from the runner cap.
18376
18962
  */
18963
+ var FrameLazyCountersSchema = object({
18964
+ framesDecoded: number(),
18965
+ framesAdmitted: number(),
18966
+ framesDroppedPixelFree: number(),
18967
+ viewsMaterialized: number(),
18968
+ viewsSkipped: number(),
18969
+ workerToRunnerBytes: number(),
18970
+ runnerToPoolRawBytes: number(),
18971
+ runnerToPoolJpegBytes: number(),
18972
+ onDemandFullFrameRequests: number(),
18973
+ onDemandCropRequests: number(),
18974
+ nativeHits: number(),
18975
+ nativeMisses: number(),
18976
+ tileHits: number(),
18977
+ tileMisses: number(),
18978
+ fallbackHits: number(),
18979
+ fallbackMisses: number(),
18980
+ retainedWritesAvoided: number(),
18981
+ residentRefs: number(),
18982
+ residentBytes: number(),
18983
+ releases: number(),
18984
+ evictions: number(),
18985
+ staleMisses: number()
18986
+ });
18987
+ var FrameLazyMetricsSchema = object({
18988
+ node: FrameLazyCountersSchema,
18989
+ cameras: array(FrameLazyCountersSchema.extend({ deviceId: number() }))
18990
+ });
18377
18991
  var RunnerLocalMetricsSchema = object({
18378
18992
  nodeId: string(),
18379
18993
  activeCameras: number(),
18380
18994
  throttledCameras: number(),
18381
18995
  avgInferenceTimeMs: number(),
18382
- queueDepth: number()
18996
+ queueDepth: number(),
18997
+ frameLazy: FrameLazyMetricsSchema.optional()
18383
18998
  });
18384
18999
  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({
18385
19000
  handle: FrameHandleSchema,
@@ -19679,6 +20294,9 @@ method(_void(), ProviderInfoSchema), method(object({ config: record(string(), un
19679
20294
  location: StorageLocationSchema,
19680
20295
  relativePath: string()
19681
20296
  }), _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" });
20297
+ /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
20298
+ var ProfileSettingsSchemaBridge = unknown().nullable();
20299
+ var ProfileSettingsBagSchema = record(string(), unknown());
19682
20300
  /**
19683
20301
  * A live terminal session hosted by the provider addon. Output and input do
19684
20302
  * NOT flow through the capability — they use the addon data plane
@@ -19708,7 +20326,14 @@ var TerminalSessionInfoSchema = object({
19708
20326
  var TerminalProfileInfoSchema = object({
19709
20327
  profileId: string(),
19710
20328
  label: string(),
19711
- description: string().optional()
20329
+ description: string().optional(),
20330
+ /** Spawn defaults the instance form copies on create. */
20331
+ executable: string().optional(),
20332
+ args: array(string()).readonly().optional(),
20333
+ cwd: string().optional(),
20334
+ environment: array(string()).readonly().optional(),
20335
+ /** ConfigUISchema for instance knobs, or null when the profile has none. */
20336
+ settingsSchema: ProfileSettingsSchemaBridge.optional()
19712
20337
  });
19713
20338
  /**
19714
20339
  * A durable operator-created Terminal instance. Profiles are templates; only
@@ -19721,7 +20346,12 @@ var TerminalInstanceInfoSchema = object({
19721
20346
  profileId: string(),
19722
20347
  profileLabel: string(),
19723
20348
  name: string(),
19724
- enabled: boolean()
20349
+ enabled: boolean(),
20350
+ executable: string(),
20351
+ args: array(string()).readonly(),
20352
+ cwd: string(),
20353
+ environment: array(string()).readonly(),
20354
+ profileSettings: ProfileSettingsBagSchema
19725
20355
  });
19726
20356
  var TerminalLegacyCameraSchema = object({
19727
20357
  stableId: string(),
@@ -19751,7 +20381,23 @@ var TerminalOutputBatchSchema = object({
19751
20381
  method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
19752
20382
  targetNodeId: string().min(1),
19753
20383
  profileId: string().min(1),
19754
- name: string().trim().min(1).max(160).optional()
20384
+ name: string().trim().min(1).max(160).optional(),
20385
+ executable: string().max(1024).optional(),
20386
+ args: array(string().max(2048)).max(64).optional(),
20387
+ cwd: string().max(1024).optional(),
20388
+ environment: array(string().max(4096)).max(64).optional(),
20389
+ profileSettings: ProfileSettingsBagSchema.optional()
20390
+ }), TerminalInstanceInfoSchema, {
20391
+ kind: "mutation",
20392
+ auth: "admin"
20393
+ }), method(object({
20394
+ instanceId: string().min(1),
20395
+ name: string().trim().min(1).max(160).optional(),
20396
+ executable: string().max(1024).optional(),
20397
+ args: array(string().max(2048)).max(64).optional(),
20398
+ cwd: string().max(1024).optional(),
20399
+ environment: array(string().max(4096)).max(64).optional(),
20400
+ profileSettings: ProfileSettingsBagSchema.optional()
19755
20401
  }), TerminalInstanceInfoSchema, {
19756
20402
  kind: "mutation",
19757
20403
  auth: "admin"
@@ -19773,7 +20419,11 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
19773
20419
  }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
19774
20420
  profileId: string(),
19775
20421
  cols: number().int().positive(),
19776
- rows: number().int().positive()
20422
+ rows: number().int().positive(),
20423
+ executable: string().max(1024).optional(),
20424
+ args: array(string().max(2048)).max(64).optional(),
20425
+ cwd: string().max(1024).optional(),
20426
+ environment: array(string().max(4096)).max(64).optional()
19777
20427
  }), TerminalSessionInfoSchema, {
19778
20428
  kind: "mutation",
19779
20429
  auth: "admin"
@@ -22555,10 +23205,10 @@ DeviceType.LawnMower, method(object({ deviceId: number().int().nonnegative() }),
22555
23205
  *
22556
23206
  * • The SDK (mobile / web client) consumes `getConnectionEndpoints()`
22557
23207
  * to receive an ordered list of candidate base URLs it should race
22558
- * on connect — LAN IPv4 first (lowest latency when on same network),
22559
- * then public hostname (if a tunnel is up), then IPv6. The SDK can
22560
- * race them with short timeouts and stick with the winner for the
22561
- * session.
23208
+ * on connect — LAN IPv4 and stable LAN IPv6 first (lowest latency
23209
+ * when on the same network), then public hostname (if a tunnel is
23210
+ * up). The SDK can race them with short timeouts and stick with the
23211
+ * winner for the session.
22562
23212
  *
22563
23213
  * Why hub-only: agents are not directly addressable by the operator's
22564
23214
  * clients — they reverse-connect to the hub. Exposing their interfaces
@@ -22713,6 +23363,17 @@ var NotificationEndpointSchema = object({
22713
23363
  /** What the ranking currently resolves to (null when nothing is reachable). */
22714
23364
  resolved: string().nullable()
22715
23365
  });
23366
+ /**
23367
+ * The URLs the SDK / viewer should race for API access. `baseUrls` empty =
23368
+ * AUTO (every LAN IPv4 + the public tunnel). `resolved` is what that choice
23369
+ * currently expands to, so the UI can show the effective set either way.
23370
+ */
23371
+ var ViewerEndpointsSchema = object({
23372
+ /** The operator's explicit race set, or empty for AUTO. */
23373
+ baseUrls: array(string()).readonly(),
23374
+ /** What the ranking currently resolves to (may be empty if nothing is up). */
23375
+ resolved: array(string()).readonly()
23376
+ });
22716
23377
  var AllowedAddressesSchema = object({
22717
23378
  /**
22718
23379
  * Allowlist of interface addresses operators have explicitly opted
@@ -22721,6 +23382,20 @@ var AllowedAddressesSchema = object({
22721
23382
  * Network Addresses admin page and persisted by the addon.
22722
23383
  */
22723
23384
  addresses: array(string()).readonly() });
23385
+ var TlsStatusSchema = object({
23386
+ mode: _enum([
23387
+ "generated",
23388
+ "uploaded",
23389
+ "disabled"
23390
+ ]),
23391
+ leafFingerprintSha256: string().nullable(),
23392
+ caFingerprintSha256: string().nullable(),
23393
+ validTo: string().nullable(),
23394
+ sans: array(string()),
23395
+ caCertPem: string().nullable(),
23396
+ reissueError: string().nullable(),
23397
+ restartRequired: boolean()
23398
+ });
22724
23399
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
22725
23400
  /**
22726
23401
  * LEGACY HINT — do not send from new code. Kept optional so clients
@@ -22730,17 +23405,31 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
22730
23405
  */
22731
23406
  port: number().int().min(1).max(65535).optional(),
22732
23407
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
22733
- * candidate. Default `true`. */
23408
+ * candidate. Default `false` — loopback is not a client route. */
22734
23409
  includeLoopback: boolean().optional(),
22735
- /** Skip IPv6 entries. Some legacy clients can't parse them.
22736
- * Default `false`. */
23410
+ /** Skip IPv6 entries. Default `false` the palette includes stable
23411
+ * LAN IPv6 (ICE/WebRTC uses dual-stack regardless). Pass `true` to
23412
+ * hide them. The viewer HTTP/WS race is `getViewerEndpoints`. */
22737
23413
  ipv4Only: boolean().optional(),
22738
23414
  /** Scheme to emit for LAN/loopback URLs. Default `'http'`.
22739
23415
  * Pass `'https'` when the caller is itself loaded over HTTPS
22740
23416
  * to avoid mixed-content blocks in the browser. The public
22741
23417
  * tunnel always emits `https://` regardless. */
22742
23418
  scheme: _enum(["http", "https"]).optional()
22743
- }), 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" });
23419
+ }), 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, {
23420
+ kind: "mutation",
23421
+ auth: "admin"
23422
+ }), method(object({
23423
+ certPem: string().min(1),
23424
+ keyPem: string().min(1),
23425
+ caPem: string().optional()
23426
+ }), TlsStatusSchema, {
23427
+ kind: "mutation",
23428
+ auth: "admin"
23429
+ }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
23430
+ kind: "mutation",
23431
+ auth: "admin"
23432
+ });
22744
23433
  object({
22745
23434
  /** Lifecycle state of the lock. `jammed` means the motor reported
22746
23435
  * failure to reach the target — operator intervention required. */
@@ -28503,6 +29192,12 @@ Object.freeze({
28503
29192
  addonId: null,
28504
29193
  access: "create"
28505
29194
  },
29195
+ "localNetwork.downloadCa": {
29196
+ capName: "local-network",
29197
+ capScope: "system",
29198
+ addonId: null,
29199
+ access: "view"
29200
+ },
28506
29201
  "localNetwork.getAllowedAddresses": {
28507
29202
  capName: "local-network",
28508
29203
  capScope: "system",
@@ -28527,18 +29222,42 @@ Object.freeze({
28527
29222
  addonId: null,
28528
29223
  access: "view"
28529
29224
  },
29225
+ "localNetwork.getTlsStatus": {
29226
+ capName: "local-network",
29227
+ capScope: "system",
29228
+ addonId: null,
29229
+ access: "view"
29230
+ },
29231
+ "localNetwork.getViewerEndpoints": {
29232
+ capName: "local-network",
29233
+ capScope: "system",
29234
+ addonId: null,
29235
+ access: "view"
29236
+ },
28530
29237
  "localNetwork.list": {
28531
29238
  capName: "local-network",
28532
29239
  capScope: "system",
28533
29240
  addonId: null,
28534
29241
  access: "view"
28535
29242
  },
29243
+ "localNetwork.regenerateCertificate": {
29244
+ capName: "local-network",
29245
+ capScope: "system",
29246
+ addonId: null,
29247
+ access: "create"
29248
+ },
28536
29249
  "localNetwork.resetAllowlistToBestMatch": {
28537
29250
  capName: "local-network",
28538
29251
  capScope: "system",
28539
29252
  addonId: null,
28540
29253
  access: "delete"
28541
29254
  },
29255
+ "localNetwork.revertToGeneratedCertificate": {
29256
+ capName: "local-network",
29257
+ capScope: "system",
29258
+ addonId: null,
29259
+ access: "create"
29260
+ },
28542
29261
  "localNetwork.setAllowedAddresses": {
28543
29262
  capName: "local-network",
28544
29263
  capScope: "system",
@@ -28551,6 +29270,18 @@ Object.freeze({
28551
29270
  addonId: null,
28552
29271
  access: "create"
28553
29272
  },
29273
+ "localNetwork.setViewerEndpoints": {
29274
+ capName: "local-network",
29275
+ capScope: "system",
29276
+ addonId: null,
29277
+ access: "create"
29278
+ },
29279
+ "localNetwork.uploadCertificate": {
29280
+ capName: "local-network",
29281
+ capScope: "system",
29282
+ addonId: null,
29283
+ access: "create"
29284
+ },
28554
29285
  "lockControl.lock": {
28555
29286
  capName: "lock-control",
28556
29287
  capScope: "device",
@@ -31431,6 +32162,12 @@ Object.freeze({
31431
32162
  addonId: null,
31432
32163
  access: "create"
31433
32164
  },
32165
+ "terminalSession.updateInstance": {
32166
+ capName: "terminal-session",
32167
+ capScope: "system",
32168
+ addonId: null,
32169
+ access: "create"
32170
+ },
31434
32171
  "terminalSession.writeInput": {
31435
32172
  capName: "terminal-session",
31436
32173
  capScope: "system",
@@ -33918,6 +34655,35 @@ Object.freeze(Object.fromEntries([{
33918
34655
  }]
33919
34656
  }].map((s) => [s.stepId, s.defaultModelId])));
33920
34657
  string().min(1);
34658
+ var CLUSTER_STEP_SETTING_FIELDS = [{
34659
+ stepId: "face-embedding",
34660
+ key: "minLandmarkFaceSize",
34661
+ label: "Min face size for recognition (detection px)",
34662
+ 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.",
34663
+ type: "slider",
34664
+ min: 0,
34665
+ max: 64,
34666
+ step: 2,
34667
+ default: 24
34668
+ }];
34669
+ function clusterStepSettingKey(stepId, fieldKey) {
34670
+ return `clusterStepSetting:${stepId}:${fieldKey}`;
34671
+ }
34672
+ var ClusterSettingNumberSchema = number().finite();
34673
+ function readClusterStepSettings(config) {
34674
+ const out = {};
34675
+ for (const field of CLUSTER_STEP_SETTING_FIELDS) {
34676
+ const parsed = ClusterSettingNumberSchema.safeParse(config[clusterStepSettingKey(field.stepId, field.key)]);
34677
+ const value = parsed.success ? parsed.data : field.default;
34678
+ const existing = out[field.stepId] ?? {};
34679
+ out[field.stepId] = {
34680
+ ...existing,
34681
+ [field.key]: value
34682
+ };
34683
+ }
34684
+ return out;
34685
+ }
34686
+ readClusterStepSettings({});
33921
34687
  object({
33922
34688
  /**
33923
34689
  * Fraction of the box's own size added on EACH side before cutting.
@@ -34167,7 +34933,8 @@ function buildEntry(metadata, artifacts) {
34167
34933
  ...metadata.inputLayout !== void 0 ? { inputLayout: metadata.inputLayout } : {},
34168
34934
  ...metadata.inputNormalization !== void 0 ? { inputNormalization: metadata.inputNormalization } : {},
34169
34935
  ...metadata.preprocessMode !== void 0 ? { preprocessMode: metadata.preprocessMode } : {},
34170
- ...metadata.faceAlignment !== void 0 ? { faceAlignment: metadata.faceAlignment } : {}
34936
+ ...metadata.faceAlignment !== void 0 ? { faceAlignment: metadata.faceAlignment } : {},
34937
+ ...metadata.classMap !== void 0 ? { classMap: metadata.classMap } : {}
34171
34938
  };
34172
34939
  }
34173
34940
  function buildModelConvertProvider(deps) {
@@ -34704,6 +35471,45 @@ var ModelNodeAvailabilitySchema = object({
34704
35471
  });
34705
35472
  /** `modelId → nodeId → availability`. */
34706
35473
  var ModelAvailabilityMapSchema = record(string(), record(string(), ModelNodeAvailabilitySchema));
35474
+ var ScryptedManifestFileSchema = object({
35475
+ relativePath: string(),
35476
+ size: number(),
35477
+ sha256: string().regex(/^[0-9a-f]{64}$/)
35478
+ });
35479
+ var ScryptedManifestEntrySchema = object({
35480
+ id: string(),
35481
+ label: string(),
35482
+ family: _enum([
35483
+ "yolov9t",
35484
+ "yolov9s",
35485
+ "yolov9m",
35486
+ "yolov9c"
35487
+ ]),
35488
+ variant: _enum(["relu", "relu_test"]),
35489
+ format: _enum([
35490
+ "coreml",
35491
+ "onnx",
35492
+ "openvino",
35493
+ "tflite"
35494
+ ]),
35495
+ precision: string(),
35496
+ inputSize: object({
35497
+ width: number(),
35498
+ height: number()
35499
+ }),
35500
+ files: array(ScryptedManifestFileSchema).readonly(),
35501
+ model: ModelCatalogEntrySchema
35502
+ });
35503
+ var ScryptedCatalogDriftSchema = object({
35504
+ manifestId: string(),
35505
+ status: _enum([
35506
+ "ok",
35507
+ "missing",
35508
+ "size-changed",
35509
+ "unavailable"
35510
+ ]),
35511
+ details: string().optional()
35512
+ });
34707
35513
  var modelStudioActions = defineCustomActions({
34708
35514
  registerCustomModel: customAction(CustomModelDescriptorSchema, object({ ok: literal(true) }), { kind: "mutation" }),
34709
35515
  removeCustomModel: customAction(object({ modelId: string() }), object({ removed: boolean() }), { kind: "mutation" }),
@@ -34770,6 +35576,16 @@ var modelStudioActions = defineCustomActions({
34770
35576
  bytes: number()
34771
35577
  }), { kind: "mutation" }),
34772
35578
  /**
35579
+ * Local Scrypted YOLO catalog (Task 7B). Listing is a projection of the
35580
+ * revision-pinned manifesto plus a read-only HF drift check. Import accepts
35581
+ * ONLY `manifestId` — host/repo/revision/path/labels/checksum stay server-owned.
35582
+ */
35583
+ listScryptedCatalog: customAction(_void(), object({
35584
+ entries: array(ScryptedManifestEntrySchema).readonly(),
35585
+ drift: array(ScryptedCatalogDriftSchema).readonly()
35586
+ })),
35587
+ importScryptedCatalogModel: customAction(object({ manifestId: string() }), CustomModelDescriptorSchema, { kind: "mutation" }),
35588
+ /**
34773
35589
  * Frigate+ personal models. The API key lives in the addon's SETTINGS
34774
35590
  * (password field) and never leaves the hub: list / test / import all run
34775
35591
  * server-side against api.frigate.video. Status deliberately reveals only
@@ -35034,6 +35850,8 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
35034
35850
  notes: c.notes
35035
35851
  })),
35036
35852
  importFrigateCatalogModel: async (input) => this.importFrigateCatalogModel(input),
35853
+ listScryptedCatalog: async () => this.listScryptedCatalog(),
35854
+ importScryptedCatalogModel: async (input) => this.importScryptedCatalogFromManifest(input),
35037
35855
  tfliteSupport: async () => {
35038
35856
  const api = this.ctx.api;
35039
35857
  if (!api) return { nodes: [] };
@@ -35226,6 +36044,78 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
35226
36044
  return hash.digest("hex");
35227
36045
  }
35228
36046
  /**
36047
+ * Projection of the local Scrypted YOLO manifesto plus a read-only HF drift
36048
+ * check. Network errors become `unavailable` rows — the manifesto is never
36049
+ * mutated and nothing is auto-imported.
36050
+ */
36051
+ async listScryptedCatalog() {
36052
+ try {
36053
+ return {
36054
+ entries: SCRYPTED_YOLO_MANIFEST,
36055
+ drift: compareScryptedManifestDrift(SCRYPTED_YOLO_MANIFEST, await fetchScryptedRemoteTree({ fetchImpl: fetch })).map(normalizeScryptedDrift)
36056
+ };
36057
+ } catch (err) {
36058
+ const details = err instanceof Error ? err.message : "Hugging Face tree unavailable";
36059
+ return {
36060
+ entries: SCRYPTED_YOLO_MANIFEST,
36061
+ drift: SCRYPTED_YOLO_MANIFEST.map((entry) => ({
36062
+ manifestId: entry.id,
36063
+ status: "unavailable",
36064
+ details
36065
+ }))
36066
+ };
36067
+ }
36068
+ }
36069
+ /**
36070
+ * Import one manifesto entry by id. Host/repo/revision/path stay server-owned.
36071
+ */
36072
+ async importScryptedCatalogFromManifest(input) {
36073
+ const modelsDir = await this.resolveModelsDir();
36074
+ const descriptor = await importScryptedCatalogModel(input, {
36075
+ stagingDir: node_path.default.join(this.ctx.dataDir ?? node_os.default.tmpdir(), "scrypted-import"),
36076
+ modelsDir,
36077
+ downloadFile: (url, destPath) => downloadFile(url, destPath, {
36078
+ redirectPolicy: assertScryptedDownloadUrl,
36079
+ maxBytes: SCRYPTED_IMPORT_MAX_FILE_BYTES,
36080
+ timeoutMs: 6e4
36081
+ }),
36082
+ driftFor: async (manifestId) => {
36083
+ try {
36084
+ const tree = await fetchScryptedRemoteTree({ fetchImpl: fetch });
36085
+ const row = compareScryptedManifestDrift(SCRYPTED_YOLO_MANIFEST.filter((entry) => entry.id === manifestId), tree)[0];
36086
+ return normalizeScryptedDrift(row ?? {
36087
+ manifestId,
36088
+ status: "missing",
36089
+ details: "entry missing from manifesto"
36090
+ });
36091
+ } catch (err) {
36092
+ return {
36093
+ manifestId,
36094
+ status: "unavailable",
36095
+ details: err instanceof Error ? err.message : "Hugging Face tree unavailable"
36096
+ };
36097
+ }
36098
+ }
36099
+ });
36100
+ await this.registerCustomModel(descriptor);
36101
+ const format = resolveScryptedImportedFormat(input.manifestId, descriptor);
36102
+ const manifestEntry = getScryptedManifestEntry(input.manifestId);
36103
+ if (!manifestEntry) throw new Error(`Imported Scrypted catalog model is missing manifesto entry ${input.manifestId}`);
36104
+ const bytes = Math.round((descriptor.entry.formats[format]?.sizeMB ?? 0) * 1e6);
36105
+ await this.availability_.update((prev) => upsertAvailability(prev, descriptor.entry.id, "hub", {
36106
+ format,
36107
+ sha256: scryptedBundleFingerprint(manifestEntry),
36108
+ bytes,
36109
+ at: Date.now()
36110
+ }));
36111
+ this.ctx.logger.info("Imported Scrypted catalog model", { meta: {
36112
+ manifestId: input.manifestId,
36113
+ modelId: descriptor.entry.id,
36114
+ bytes
36115
+ } });
36116
+ return descriptor;
36117
+ }
36118
+ /**
35229
36119
  * Import a curated PUBLIC Frigate catalog model: download SERVER-SIDE into
35230
36120
  * the hub's modelsDir (size-verified against the curated declaration),
35231
36121
  * register the descriptor with the PUBLIC url (nodes download on demand),