@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
@@ -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,7 +103,496 @@ async function downloadFile(url, destPath, onProgress) {
67
103
  promisify(brotliCompress);
68
104
  promisify(gzip);
69
105
  //#endregion
70
- //#region ../types/dist/event-category-XfKNtfCc.mjs
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
595
+ //#region ../types/dist/event-category-CIa_iT6b.mjs
71
596
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
72
597
  EventCategory["SystemBoot"] = "system.boot";
73
598
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -83,6 +608,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
83
608
  */
84
609
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
85
610
  /**
611
+ * The hub reissued its own TLS certificate at boot (`ensureTlsCert`).
612
+ * Emitted only when the material on disk actually changed, so an
613
+ * operator who trusted the old certificate by hand is told rather than
614
+ * discovering it as a browser error. Payload `TlsCertChangedPayload`.
615
+ *
616
+ * Rule: docs/decisions/adr-0227-*.md
617
+ */
618
+ EventCategory["SystemTlsCertChanged"] = "system.tls-cert-changed";
619
+ /**
86
620
  * A newer addon or server-root package version was found by the
87
621
  * authoritative registry check. Emitted once when any observed
88
622
  * `latestVersion` changes (or a package/node first appears behind);
@@ -8122,6 +8656,15 @@ var LabelDefinitionSchema = object({
8122
8656
  description: string().optional(),
8123
8657
  icon: string().optional()
8124
8658
  });
8659
+ var ClassMapDefinitionSchema = object({
8660
+ mapping: record(string(), _enum([
8661
+ "person",
8662
+ "vehicle",
8663
+ "animal",
8664
+ "package"
8665
+ ])),
8666
+ preserveOriginal: boolean()
8667
+ });
8125
8668
  var MODEL_FORMATS = [
8126
8669
  "onnx",
8127
8670
  "coreml",
@@ -8300,7 +8843,13 @@ var ModelCatalogEntrySchema = object({
8300
8843
  * `id` stays the source of truth for resolution/download/persistence; grouping
8301
8844
  * is a presentation overlay resolved back to an `id`.
8302
8845
  */
8303
- 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()
8304
8853
  });
8305
8854
  var ConvertTargetSchema = discriminatedUnion("format", [object({
8306
8855
  format: literal("openvino"),
@@ -8329,7 +8878,8 @@ var ModelConvertMetadataSchema = object({
8329
8878
  "ocr",
8330
8879
  "segmentation"
8331
8880
  ]),
8332
- faceAlignment: boolean().optional()
8881
+ faceAlignment: boolean().optional(),
8882
+ classMap: ClassMapDefinitionSchema.optional()
8333
8883
  });
8334
8884
  var ConvertArtifactSchema = object({
8335
8885
  format: _enum(MODEL_FORMATS),
@@ -17361,6 +17911,33 @@ var NativeCropRefSchema = object({
17361
17911
  h: number()
17362
17912
  })
17363
17913
  });
17914
+ object({
17915
+ crop: object({
17916
+ left: number(),
17917
+ top: number(),
17918
+ width: number().positive(),
17919
+ height: number().positive()
17920
+ }).optional(),
17921
+ content: object({
17922
+ width: number().int().positive(),
17923
+ height: number().int().positive()
17924
+ }),
17925
+ fit: _enum(["stretch", "contain"]),
17926
+ format: _enum([
17927
+ "rgb",
17928
+ "gray",
17929
+ "jpeg"
17930
+ ])
17931
+ });
17932
+ var FrameRefSchema = object({
17933
+ registryId: string().min(1),
17934
+ id: string().min(1),
17935
+ width: number().int().positive(),
17936
+ height: number().int().positive(),
17937
+ format: _enum(["rgb", "gray"]),
17938
+ timestamp: number(),
17939
+ capturedAt: number().optional()
17940
+ });
17364
17941
  var ModelFormatSchema$1 = _enum([
17365
17942
  "onnx",
17366
17943
  "coreml",
@@ -17605,6 +18182,12 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
17605
18182
  steps: array(PipelineStepInputSchema).min(1),
17606
18183
  frame: FrameInputSchema.optional(),
17607
18184
  /**
18185
+ * Process-local lazy frame. Valid only when caller and provider resolve
18186
+ * in the same execution-group process; split/cross-node callers use
18187
+ * `frame`/`image` inline compatibility instead.
18188
+ */
18189
+ frameRef: FrameRefSchema.optional(),
18190
+ /**
17608
18191
  * CB5 shm passthrough — a `FrameHandle` naming the same ring slot
17609
18192
  * the decoded pixels live in. One more member of the one-of
17610
18193
  * frame/frameHandle/image/imageBase64/referenceImage group.
@@ -17860,7 +18443,10 @@ var NativeCropResultSchema = object({
17860
18443
  * Which source served this crop, so a quality-sensitive consumer (the native
17861
18444
  * `keyFrame`) can reject a degraded fallback:
17862
18445
  * - `native` — cut from the decode worker's retained NATIVE surface (the
17863
- * quality path).
18446
+ * quality path). A subject-tile serve is also native-resolution and stays
18447
+ * `native` here: the public enum cannot name `tile` without a breaking cap
18448
+ * change. Runner telemetry distinguishes lease vs tile via `source` on the
18449
+ * internal crop result (`nativeHits` vs `tileHits`).
17864
18450
  * - `ram-fullframe` — the native surface MISSED but the request was full-frame,
17865
18451
  * so the ≤640 RAM `RetainedFrameStore` served it (honest lower-res; legit for
17866
18452
  * the blank-frame guard / 640-snapshot resolve, NOT for the clean keyFrame).
@@ -18351,12 +18937,41 @@ var RunnerLocalLoadSchema = object({
18351
18937
  * legacy `OrchestratorMetricsSchema` shape so existing dashboards keep
18352
18938
  * working unchanged when they switch to reading from the runner cap.
18353
18939
  */
18940
+ var FrameLazyCountersSchema = object({
18941
+ framesDecoded: number(),
18942
+ framesAdmitted: number(),
18943
+ framesDroppedPixelFree: number(),
18944
+ viewsMaterialized: number(),
18945
+ viewsSkipped: number(),
18946
+ workerToRunnerBytes: number(),
18947
+ runnerToPoolRawBytes: number(),
18948
+ runnerToPoolJpegBytes: number(),
18949
+ onDemandFullFrameRequests: number(),
18950
+ onDemandCropRequests: number(),
18951
+ nativeHits: number(),
18952
+ nativeMisses: number(),
18953
+ tileHits: number(),
18954
+ tileMisses: number(),
18955
+ fallbackHits: number(),
18956
+ fallbackMisses: number(),
18957
+ retainedWritesAvoided: number(),
18958
+ residentRefs: number(),
18959
+ residentBytes: number(),
18960
+ releases: number(),
18961
+ evictions: number(),
18962
+ staleMisses: number()
18963
+ });
18964
+ var FrameLazyMetricsSchema = object({
18965
+ node: FrameLazyCountersSchema,
18966
+ cameras: array(FrameLazyCountersSchema.extend({ deviceId: number() }))
18967
+ });
18354
18968
  var RunnerLocalMetricsSchema = object({
18355
18969
  nodeId: string(),
18356
18970
  activeCameras: number(),
18357
18971
  throttledCameras: number(),
18358
18972
  avgInferenceTimeMs: number(),
18359
- queueDepth: number()
18973
+ queueDepth: number(),
18974
+ frameLazy: FrameLazyMetricsSchema.optional()
18360
18975
  });
18361
18976
  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({
18362
18977
  handle: FrameHandleSchema,
@@ -19656,6 +20271,9 @@ method(_void(), ProviderInfoSchema), method(object({ config: record(string(), un
19656
20271
  location: StorageLocationSchema,
19657
20272
  relativePath: string()
19658
20273
  }), _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" });
20274
+ /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
20275
+ var ProfileSettingsSchemaBridge = unknown().nullable();
20276
+ var ProfileSettingsBagSchema = record(string(), unknown());
19659
20277
  /**
19660
20278
  * A live terminal session hosted by the provider addon. Output and input do
19661
20279
  * NOT flow through the capability — they use the addon data plane
@@ -19685,7 +20303,14 @@ var TerminalSessionInfoSchema = object({
19685
20303
  var TerminalProfileInfoSchema = object({
19686
20304
  profileId: string(),
19687
20305
  label: string(),
19688
- description: string().optional()
20306
+ description: string().optional(),
20307
+ /** Spawn defaults the instance form copies on create. */
20308
+ executable: string().optional(),
20309
+ args: array(string()).readonly().optional(),
20310
+ cwd: string().optional(),
20311
+ environment: array(string()).readonly().optional(),
20312
+ /** ConfigUISchema for instance knobs, or null when the profile has none. */
20313
+ settingsSchema: ProfileSettingsSchemaBridge.optional()
19689
20314
  });
19690
20315
  /**
19691
20316
  * A durable operator-created Terminal instance. Profiles are templates; only
@@ -19698,7 +20323,12 @@ var TerminalInstanceInfoSchema = object({
19698
20323
  profileId: string(),
19699
20324
  profileLabel: string(),
19700
20325
  name: string(),
19701
- enabled: boolean()
20326
+ enabled: boolean(),
20327
+ executable: string(),
20328
+ args: array(string()).readonly(),
20329
+ cwd: string(),
20330
+ environment: array(string()).readonly(),
20331
+ profileSettings: ProfileSettingsBagSchema
19702
20332
  });
19703
20333
  var TerminalLegacyCameraSchema = object({
19704
20334
  stableId: string(),
@@ -19728,7 +20358,23 @@ var TerminalOutputBatchSchema = object({
19728
20358
  method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
19729
20359
  targetNodeId: string().min(1),
19730
20360
  profileId: string().min(1),
19731
- name: string().trim().min(1).max(160).optional()
20361
+ name: string().trim().min(1).max(160).optional(),
20362
+ executable: string().max(1024).optional(),
20363
+ args: array(string().max(2048)).max(64).optional(),
20364
+ cwd: string().max(1024).optional(),
20365
+ environment: array(string().max(4096)).max(64).optional(),
20366
+ profileSettings: ProfileSettingsBagSchema.optional()
20367
+ }), TerminalInstanceInfoSchema, {
20368
+ kind: "mutation",
20369
+ auth: "admin"
20370
+ }), method(object({
20371
+ instanceId: string().min(1),
20372
+ name: string().trim().min(1).max(160).optional(),
20373
+ executable: string().max(1024).optional(),
20374
+ args: array(string().max(2048)).max(64).optional(),
20375
+ cwd: string().max(1024).optional(),
20376
+ environment: array(string().max(4096)).max(64).optional(),
20377
+ profileSettings: ProfileSettingsBagSchema.optional()
19732
20378
  }), TerminalInstanceInfoSchema, {
19733
20379
  kind: "mutation",
19734
20380
  auth: "admin"
@@ -19750,7 +20396,11 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
19750
20396
  }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
19751
20397
  profileId: string(),
19752
20398
  cols: number().int().positive(),
19753
- rows: number().int().positive()
20399
+ rows: number().int().positive(),
20400
+ executable: string().max(1024).optional(),
20401
+ args: array(string().max(2048)).max(64).optional(),
20402
+ cwd: string().max(1024).optional(),
20403
+ environment: array(string().max(4096)).max(64).optional()
19754
20404
  }), TerminalSessionInfoSchema, {
19755
20405
  kind: "mutation",
19756
20406
  auth: "admin"
@@ -22532,10 +23182,10 @@ DeviceType.LawnMower, method(object({ deviceId: number().int().nonnegative() }),
22532
23182
  *
22533
23183
  * • The SDK (mobile / web client) consumes `getConnectionEndpoints()`
22534
23184
  * to receive an ordered list of candidate base URLs it should race
22535
- * on connect — LAN IPv4 first (lowest latency when on same network),
22536
- * then public hostname (if a tunnel is up), then IPv6. The SDK can
22537
- * race them with short timeouts and stick with the winner for the
22538
- * session.
23185
+ * on connect — LAN IPv4 and stable LAN IPv6 first (lowest latency
23186
+ * when on the same network), then public hostname (if a tunnel is
23187
+ * up). The SDK can race them with short timeouts and stick with the
23188
+ * winner for the session.
22539
23189
  *
22540
23190
  * Why hub-only: agents are not directly addressable by the operator's
22541
23191
  * clients — they reverse-connect to the hub. Exposing their interfaces
@@ -22690,6 +23340,17 @@ var NotificationEndpointSchema = object({
22690
23340
  /** What the ranking currently resolves to (null when nothing is reachable). */
22691
23341
  resolved: string().nullable()
22692
23342
  });
23343
+ /**
23344
+ * The URLs the SDK / viewer should race for API access. `baseUrls` empty =
23345
+ * AUTO (every LAN IPv4 + the public tunnel). `resolved` is what that choice
23346
+ * currently expands to, so the UI can show the effective set either way.
23347
+ */
23348
+ var ViewerEndpointsSchema = object({
23349
+ /** The operator's explicit race set, or empty for AUTO. */
23350
+ baseUrls: array(string()).readonly(),
23351
+ /** What the ranking currently resolves to (may be empty if nothing is up). */
23352
+ resolved: array(string()).readonly()
23353
+ });
22693
23354
  var AllowedAddressesSchema = object({
22694
23355
  /**
22695
23356
  * Allowlist of interface addresses operators have explicitly opted
@@ -22698,6 +23359,20 @@ var AllowedAddressesSchema = object({
22698
23359
  * Network Addresses admin page and persisted by the addon.
22699
23360
  */
22700
23361
  addresses: array(string()).readonly() });
23362
+ var TlsStatusSchema = object({
23363
+ mode: _enum([
23364
+ "generated",
23365
+ "uploaded",
23366
+ "disabled"
23367
+ ]),
23368
+ leafFingerprintSha256: string().nullable(),
23369
+ caFingerprintSha256: string().nullable(),
23370
+ validTo: string().nullable(),
23371
+ sans: array(string()),
23372
+ caCertPem: string().nullable(),
23373
+ reissueError: string().nullable(),
23374
+ restartRequired: boolean()
23375
+ });
22701
23376
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
22702
23377
  /**
22703
23378
  * LEGACY HINT — do not send from new code. Kept optional so clients
@@ -22707,17 +23382,31 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
22707
23382
  */
22708
23383
  port: number().int().min(1).max(65535).optional(),
22709
23384
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
22710
- * candidate. Default `true`. */
23385
+ * candidate. Default `false` — loopback is not a client route. */
22711
23386
  includeLoopback: boolean().optional(),
22712
- /** Skip IPv6 entries. Some legacy clients can't parse them.
22713
- * Default `false`. */
23387
+ /** Skip IPv6 entries. Default `false` the palette includes stable
23388
+ * LAN IPv6 (ICE/WebRTC uses dual-stack regardless). Pass `true` to
23389
+ * hide them. The viewer HTTP/WS race is `getViewerEndpoints`. */
22714
23390
  ipv4Only: boolean().optional(),
22715
23391
  /** Scheme to emit for LAN/loopback URLs. Default `'http'`.
22716
23392
  * Pass `'https'` when the caller is itself loaded over HTTPS
22717
23393
  * to avoid mixed-content blocks in the browser. The public
22718
23394
  * tunnel always emits `https://` regardless. */
22719
23395
  scheme: _enum(["http", "https"]).optional()
22720
- }), 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" });
23396
+ }), 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, {
23397
+ kind: "mutation",
23398
+ auth: "admin"
23399
+ }), method(object({
23400
+ certPem: string().min(1),
23401
+ keyPem: string().min(1),
23402
+ caPem: string().optional()
23403
+ }), TlsStatusSchema, {
23404
+ kind: "mutation",
23405
+ auth: "admin"
23406
+ }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
23407
+ kind: "mutation",
23408
+ auth: "admin"
23409
+ });
22721
23410
  object({
22722
23411
  /** Lifecycle state of the lock. `jammed` means the motor reported
22723
23412
  * failure to reach the target — operator intervention required. */
@@ -28480,6 +29169,12 @@ Object.freeze({
28480
29169
  addonId: null,
28481
29170
  access: "create"
28482
29171
  },
29172
+ "localNetwork.downloadCa": {
29173
+ capName: "local-network",
29174
+ capScope: "system",
29175
+ addonId: null,
29176
+ access: "view"
29177
+ },
28483
29178
  "localNetwork.getAllowedAddresses": {
28484
29179
  capName: "local-network",
28485
29180
  capScope: "system",
@@ -28504,18 +29199,42 @@ Object.freeze({
28504
29199
  addonId: null,
28505
29200
  access: "view"
28506
29201
  },
29202
+ "localNetwork.getTlsStatus": {
29203
+ capName: "local-network",
29204
+ capScope: "system",
29205
+ addonId: null,
29206
+ access: "view"
29207
+ },
29208
+ "localNetwork.getViewerEndpoints": {
29209
+ capName: "local-network",
29210
+ capScope: "system",
29211
+ addonId: null,
29212
+ access: "view"
29213
+ },
28507
29214
  "localNetwork.list": {
28508
29215
  capName: "local-network",
28509
29216
  capScope: "system",
28510
29217
  addonId: null,
28511
29218
  access: "view"
28512
29219
  },
29220
+ "localNetwork.regenerateCertificate": {
29221
+ capName: "local-network",
29222
+ capScope: "system",
29223
+ addonId: null,
29224
+ access: "create"
29225
+ },
28513
29226
  "localNetwork.resetAllowlistToBestMatch": {
28514
29227
  capName: "local-network",
28515
29228
  capScope: "system",
28516
29229
  addonId: null,
28517
29230
  access: "delete"
28518
29231
  },
29232
+ "localNetwork.revertToGeneratedCertificate": {
29233
+ capName: "local-network",
29234
+ capScope: "system",
29235
+ addonId: null,
29236
+ access: "create"
29237
+ },
28519
29238
  "localNetwork.setAllowedAddresses": {
28520
29239
  capName: "local-network",
28521
29240
  capScope: "system",
@@ -28528,6 +29247,18 @@ Object.freeze({
28528
29247
  addonId: null,
28529
29248
  access: "create"
28530
29249
  },
29250
+ "localNetwork.setViewerEndpoints": {
29251
+ capName: "local-network",
29252
+ capScope: "system",
29253
+ addonId: null,
29254
+ access: "create"
29255
+ },
29256
+ "localNetwork.uploadCertificate": {
29257
+ capName: "local-network",
29258
+ capScope: "system",
29259
+ addonId: null,
29260
+ access: "create"
29261
+ },
28531
29262
  "lockControl.lock": {
28532
29263
  capName: "lock-control",
28533
29264
  capScope: "device",
@@ -31408,6 +32139,12 @@ Object.freeze({
31408
32139
  addonId: null,
31409
32140
  access: "create"
31410
32141
  },
32142
+ "terminalSession.updateInstance": {
32143
+ capName: "terminal-session",
32144
+ capScope: "system",
32145
+ addonId: null,
32146
+ access: "create"
32147
+ },
31411
32148
  "terminalSession.writeInput": {
31412
32149
  capName: "terminal-session",
31413
32150
  capScope: "system",
@@ -33895,6 +34632,35 @@ Object.freeze(Object.fromEntries([{
33895
34632
  }]
33896
34633
  }].map((s) => [s.stepId, s.defaultModelId])));
33897
34634
  string().min(1);
34635
+ var CLUSTER_STEP_SETTING_FIELDS = [{
34636
+ stepId: "face-embedding",
34637
+ key: "minLandmarkFaceSize",
34638
+ label: "Min face size for recognition (detection px)",
34639
+ 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.",
34640
+ type: "slider",
34641
+ min: 0,
34642
+ max: 64,
34643
+ step: 2,
34644
+ default: 24
34645
+ }];
34646
+ function clusterStepSettingKey(stepId, fieldKey) {
34647
+ return `clusterStepSetting:${stepId}:${fieldKey}`;
34648
+ }
34649
+ var ClusterSettingNumberSchema = number().finite();
34650
+ function readClusterStepSettings(config) {
34651
+ const out = {};
34652
+ for (const field of CLUSTER_STEP_SETTING_FIELDS) {
34653
+ const parsed = ClusterSettingNumberSchema.safeParse(config[clusterStepSettingKey(field.stepId, field.key)]);
34654
+ const value = parsed.success ? parsed.data : field.default;
34655
+ const existing = out[field.stepId] ?? {};
34656
+ out[field.stepId] = {
34657
+ ...existing,
34658
+ [field.key]: value
34659
+ };
34660
+ }
34661
+ return out;
34662
+ }
34663
+ readClusterStepSettings({});
33898
34664
  object({
33899
34665
  /**
33900
34666
  * Fraction of the box's own size added on EACH side before cutting.
@@ -34144,7 +34910,8 @@ function buildEntry(metadata, artifacts) {
34144
34910
  ...metadata.inputLayout !== void 0 ? { inputLayout: metadata.inputLayout } : {},
34145
34911
  ...metadata.inputNormalization !== void 0 ? { inputNormalization: metadata.inputNormalization } : {},
34146
34912
  ...metadata.preprocessMode !== void 0 ? { preprocessMode: metadata.preprocessMode } : {},
34147
- ...metadata.faceAlignment !== void 0 ? { faceAlignment: metadata.faceAlignment } : {}
34913
+ ...metadata.faceAlignment !== void 0 ? { faceAlignment: metadata.faceAlignment } : {},
34914
+ ...metadata.classMap !== void 0 ? { classMap: metadata.classMap } : {}
34148
34915
  };
34149
34916
  }
34150
34917
  function buildModelConvertProvider(deps) {
@@ -34681,6 +35448,45 @@ var ModelNodeAvailabilitySchema = object({
34681
35448
  });
34682
35449
  /** `modelId → nodeId → availability`. */
34683
35450
  var ModelAvailabilityMapSchema = record(string(), record(string(), ModelNodeAvailabilitySchema));
35451
+ var ScryptedManifestFileSchema = object({
35452
+ relativePath: string(),
35453
+ size: number(),
35454
+ sha256: string().regex(/^[0-9a-f]{64}$/)
35455
+ });
35456
+ var ScryptedManifestEntrySchema = object({
35457
+ id: string(),
35458
+ label: string(),
35459
+ family: _enum([
35460
+ "yolov9t",
35461
+ "yolov9s",
35462
+ "yolov9m",
35463
+ "yolov9c"
35464
+ ]),
35465
+ variant: _enum(["relu", "relu_test"]),
35466
+ format: _enum([
35467
+ "coreml",
35468
+ "onnx",
35469
+ "openvino",
35470
+ "tflite"
35471
+ ]),
35472
+ precision: string(),
35473
+ inputSize: object({
35474
+ width: number(),
35475
+ height: number()
35476
+ }),
35477
+ files: array(ScryptedManifestFileSchema).readonly(),
35478
+ model: ModelCatalogEntrySchema
35479
+ });
35480
+ var ScryptedCatalogDriftSchema = object({
35481
+ manifestId: string(),
35482
+ status: _enum([
35483
+ "ok",
35484
+ "missing",
35485
+ "size-changed",
35486
+ "unavailable"
35487
+ ]),
35488
+ details: string().optional()
35489
+ });
34684
35490
  var modelStudioActions = defineCustomActions({
34685
35491
  registerCustomModel: customAction(CustomModelDescriptorSchema, object({ ok: literal(true) }), { kind: "mutation" }),
34686
35492
  removeCustomModel: customAction(object({ modelId: string() }), object({ removed: boolean() }), { kind: "mutation" }),
@@ -34747,6 +35553,16 @@ var modelStudioActions = defineCustomActions({
34747
35553
  bytes: number()
34748
35554
  }), { kind: "mutation" }),
34749
35555
  /**
35556
+ * Local Scrypted YOLO catalog (Task 7B). Listing is a projection of the
35557
+ * revision-pinned manifesto plus a read-only HF drift check. Import accepts
35558
+ * ONLY `manifestId` — host/repo/revision/path/labels/checksum stay server-owned.
35559
+ */
35560
+ listScryptedCatalog: customAction(_void(), object({
35561
+ entries: array(ScryptedManifestEntrySchema).readonly(),
35562
+ drift: array(ScryptedCatalogDriftSchema).readonly()
35563
+ })),
35564
+ importScryptedCatalogModel: customAction(object({ manifestId: string() }), CustomModelDescriptorSchema, { kind: "mutation" }),
35565
+ /**
34750
35566
  * Frigate+ personal models. The API key lives in the addon's SETTINGS
34751
35567
  * (password field) and never leaves the hub: list / test / import all run
34752
35568
  * server-side against api.frigate.video. Status deliberately reveals only
@@ -35011,6 +35827,8 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
35011
35827
  notes: c.notes
35012
35828
  })),
35013
35829
  importFrigateCatalogModel: async (input) => this.importFrigateCatalogModel(input),
35830
+ listScryptedCatalog: async () => this.listScryptedCatalog(),
35831
+ importScryptedCatalogModel: async (input) => this.importScryptedCatalogFromManifest(input),
35014
35832
  tfliteSupport: async () => {
35015
35833
  const api = this.ctx.api;
35016
35834
  if (!api) return { nodes: [] };
@@ -35203,6 +36021,78 @@ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
35203
36021
  return hash.digest("hex");
35204
36022
  }
35205
36023
  /**
36024
+ * Projection of the local Scrypted YOLO manifesto plus a read-only HF drift
36025
+ * check. Network errors become `unavailable` rows — the manifesto is never
36026
+ * mutated and nothing is auto-imported.
36027
+ */
36028
+ async listScryptedCatalog() {
36029
+ try {
36030
+ return {
36031
+ entries: SCRYPTED_YOLO_MANIFEST,
36032
+ drift: compareScryptedManifestDrift(SCRYPTED_YOLO_MANIFEST, await fetchScryptedRemoteTree({ fetchImpl: fetch })).map(normalizeScryptedDrift)
36033
+ };
36034
+ } catch (err) {
36035
+ const details = err instanceof Error ? err.message : "Hugging Face tree unavailable";
36036
+ return {
36037
+ entries: SCRYPTED_YOLO_MANIFEST,
36038
+ drift: SCRYPTED_YOLO_MANIFEST.map((entry) => ({
36039
+ manifestId: entry.id,
36040
+ status: "unavailable",
36041
+ details
36042
+ }))
36043
+ };
36044
+ }
36045
+ }
36046
+ /**
36047
+ * Import one manifesto entry by id. Host/repo/revision/path stay server-owned.
36048
+ */
36049
+ async importScryptedCatalogFromManifest(input) {
36050
+ const modelsDir = await this.resolveModelsDir();
36051
+ const descriptor = await importScryptedCatalogModel(input, {
36052
+ stagingDir: path.join(this.ctx.dataDir ?? os.tmpdir(), "scrypted-import"),
36053
+ modelsDir,
36054
+ downloadFile: (url, destPath) => downloadFile(url, destPath, {
36055
+ redirectPolicy: assertScryptedDownloadUrl,
36056
+ maxBytes: SCRYPTED_IMPORT_MAX_FILE_BYTES,
36057
+ timeoutMs: 6e4
36058
+ }),
36059
+ driftFor: async (manifestId) => {
36060
+ try {
36061
+ const tree = await fetchScryptedRemoteTree({ fetchImpl: fetch });
36062
+ const row = compareScryptedManifestDrift(SCRYPTED_YOLO_MANIFEST.filter((entry) => entry.id === manifestId), tree)[0];
36063
+ return normalizeScryptedDrift(row ?? {
36064
+ manifestId,
36065
+ status: "missing",
36066
+ details: "entry missing from manifesto"
36067
+ });
36068
+ } catch (err) {
36069
+ return {
36070
+ manifestId,
36071
+ status: "unavailable",
36072
+ details: err instanceof Error ? err.message : "Hugging Face tree unavailable"
36073
+ };
36074
+ }
36075
+ }
36076
+ });
36077
+ await this.registerCustomModel(descriptor);
36078
+ const format = resolveScryptedImportedFormat(input.manifestId, descriptor);
36079
+ const manifestEntry = getScryptedManifestEntry(input.manifestId);
36080
+ if (!manifestEntry) throw new Error(`Imported Scrypted catalog model is missing manifesto entry ${input.manifestId}`);
36081
+ const bytes = Math.round((descriptor.entry.formats[format]?.sizeMB ?? 0) * 1e6);
36082
+ await this.availability_.update((prev) => upsertAvailability(prev, descriptor.entry.id, "hub", {
36083
+ format,
36084
+ sha256: scryptedBundleFingerprint(manifestEntry),
36085
+ bytes,
36086
+ at: Date.now()
36087
+ }));
36088
+ this.ctx.logger.info("Imported Scrypted catalog model", { meta: {
36089
+ manifestId: input.manifestId,
36090
+ modelId: descriptor.entry.id,
36091
+ bytes
36092
+ } });
36093
+ return descriptor;
36094
+ }
36095
+ /**
35206
36096
  * Import a curated PUBLIC Frigate catalog model: download SERVER-SIDE into
35207
36097
  * the hub's modelsDir (size-verified against the curated declaration),
35208
36098
  * register the descriptor with the PUBLIC url (nodes download on demand),