@camstack/addon-model-studio 1.1.32 → 1.1.34

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-sZkppmGH.mjs → MotionZonesSettings-W4fQr3y3.mjs} +2 -2
  2. package/dist/{PrivacyMaskSettings-C4IQ_tob.mjs → PrivacyMaskSettings-CtKzQjYD.mjs} +4 -4
  3. package/dist/{SceneMonitorEditor-Dv55kWSO.mjs → SceneMonitorEditor-CM3z_N4Q.mjs} +3 -3
  4. package/dist/_stub.js +3757 -3212
  5. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-Cg1VXUiH.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-BGvyoZxl.mjs} +4 -4
  6. package/dist/_virtual_mf___mfe_internal__addon_model_studio_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Boi8PUUw.mjs +26 -0
  7. package/dist/addon-model-studio.css +1 -1
  8. package/dist/{hostInit-DXmPMLUp.mjs → hostInit-BKRnEFn7.mjs} +3 -3
  9. package/dist/model-studio.addon.js +746 -19
  10. package/dist/model-studio.addon.mjs +746 -19
  11. package/dist/{player-overlays-CS5IM4vB.mjs → player-overlays-CNkUzVZI.mjs} +1 -1
  12. package/dist/remoteEntry.js +1 -1
  13. package/dist/{responsive-CWtuZhDz.mjs → responsive-D-JIMJTX.mjs} +1 -1
  14. package/dist/{square-OXaWZ-Np.mjs → square-CBwtzgNH.mjs} +1 -1
  15. package/dist/{trash-2-BtfCrPnQ.mjs → trash-2-CiJaurwE.mjs} +1 -1
  16. package/dist/{use-device-snapshot-Bx87WhSL.mjs → use-device-snapshot-DpcJhtJX.mjs} +1 -1
  17. package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-CnPUM5JZ.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-BqGMvm3d.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-Do4Kqwhj.mjs +0 -26
@@ -67,7 +67,7 @@ async function downloadFile(url, destPath, onProgress) {
67
67
  promisify(brotliCompress);
68
68
  promisify(gzip);
69
69
  //#endregion
70
- //#region ../types/dist/event-category-XfKNtfCc.mjs
70
+ //#region ../types/dist/event-category-CIa_iT6b.mjs
71
71
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
72
72
  EventCategory["SystemBoot"] = "system.boot";
73
73
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -83,6 +83,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
83
83
  */
84
84
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
85
85
  /**
86
+ * The hub reissued its own TLS certificate at boot (`ensureTlsCert`).
87
+ * Emitted only when the material on disk actually changed, so an
88
+ * operator who trusted the old certificate by hand is told rather than
89
+ * discovering it as a browser error. Payload `TlsCertChangedPayload`.
90
+ *
91
+ * Rule: docs/decisions/adr-0227-*.md
92
+ */
93
+ EventCategory["SystemTlsCertChanged"] = "system.tls-cert-changed";
94
+ /**
86
95
  * A newer addon or server-root package version was found by the
87
96
  * authoritative registry check. Emitted once when any observed
88
97
  * `latestVersion` changes (or a package/node first appears behind);
@@ -18791,6 +18800,12 @@ var CameraStatusSchema = object({
18791
18800
  /** Unix timestamp (ms) when this snapshot was composed server-side. */
18792
18801
  fetchedAt: number()
18793
18802
  });
18803
+ var InferenceDeviceExclusionReasonSchema = _enum([
18804
+ "disabled",
18805
+ "unavailable",
18806
+ "cannot-host-camera-root",
18807
+ "accelerator-preferred"
18808
+ ]);
18794
18809
  var NodeInferenceDeviceSchema = object({
18795
18810
  /** Stable per-node device key, e.g. `openvino:npu`, `edgetpu:usb`, `cpu`. */
18796
18811
  key: string(),
@@ -18821,7 +18836,17 @@ var NodeInferenceDeviceSchema = object({
18821
18836
  * available per format; this is the stored selection that becomes the
18822
18837
  * default for EVERY camera landing on this accelerator.
18823
18838
  */
18824
- steps: record(string(), DeviceStepConfigSchema).optional()
18839
+ steps: record(string(), DeviceStepConfigSchema).optional(),
18840
+ /**
18841
+ * `null` when the device IS a camera-root candidate on this node; otherwise
18842
+ * the reason the dispatcher drops it. Computed by the SAME
18843
+ * `resolveInferenceDeviceEligibility` the dispatcher runs, so this view can
18844
+ * never disagree with the election — deriving it in the UI from
18845
+ * `enabled`/`available` would silently miss `cannot-host-camera-root` (needs
18846
+ * the node's model catalog) and `accelerator-preferred` (needs the node-wide
18847
+ * "an accelerator is serving" predicate).
18848
+ */
18849
+ exclusion: InferenceDeviceExclusionReasonSchema.nullable()
18825
18850
  });
18826
18851
  var NodeInferenceDevicesSchema = object({
18827
18852
  nodeId: string(),
@@ -22579,7 +22604,12 @@ var ListResultSchema = object({
22579
22604
  probedAt: number()
22580
22605
  });
22581
22606
  var PreferredSchema = LocalInterfaceSchema.nullable();
22582
- var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
22607
+ /**
22608
+ * Candidate base URL for the SDK to race on connect. Order matters —
22609
+ * the SDK should attempt these top-to-bottom with a short per-candidate
22610
+ * timeout (e.g. 1500ms) and cache the winner for the session.
22611
+ */
22612
+ var ConnectionEndpointSchema = object({
22583
22613
  /** Operator-facing label (e.g. "LAN — en0", "Public tunnel"). */
22584
22614
  label: string(),
22585
22615
  /** Fully-formed base URL with scheme + host + port. */
@@ -22622,7 +22652,42 @@ var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
22622
22652
  * ordering between polls.
22623
22653
  */
22624
22654
  priority: number()
22625
- })).readonly() });
22655
+ });
22656
+ /**
22657
+ * Where the advertised local port came from. Ordered most → least
22658
+ * authoritative, and the whole point of returning it: a client must be able to
22659
+ * tell a FACT about the hub's socket from an echo of its own guess.
22660
+ */
22661
+ var LocalPortSourceEnum = _enum([
22662
+ "server-config",
22663
+ "server-env",
22664
+ "caller-hint",
22665
+ "default"
22666
+ ]);
22667
+ /** The port every LAN/loopback `baseUrl` in the same result was built with. */
22668
+ var AdvertisedLocalPortSchema = object({
22669
+ port: number().int().min(1).max(65535),
22670
+ source: LocalPortSourceEnum
22671
+ });
22672
+ var GetConnectionEndpointsResultSchema = object({
22673
+ endpoints: array(ConnectionEndpointSchema).readonly(),
22674
+ /**
22675
+ * The port the hub built the LAN/loopback URLs with, and where that number
22676
+ * came from.
22677
+ *
22678
+ * Returned rather than merely applied, because "the URL is right" and "the
22679
+ * client can KNOW the URL is right" are different properties. A client that
22680
+ * only sees a corrected URL cannot distinguish a hub that fixed the port from
22681
+ * a hub that echoed the port the client sent, so it cannot decide whether to
22682
+ * race the candidate or discard it. With `source` it can: anything but
22683
+ * `caller-hint` is the hub's own socket.
22684
+ *
22685
+ * Absent on hubs predating this field — a client that finds it missing is
22686
+ * talking to an echoing hub and must degrade exactly as it does for
22687
+ * `caller-hint`.
22688
+ */
22689
+ localPort: AdvertisedLocalPortSchema
22690
+ });
22626
22691
  /**
22627
22692
  * The chosen outbound endpoint for notification artifacts. `baseUrl: null` =
22628
22693
  * AUTO (resolved from the candidate ranking at send time); `resolved` reports
@@ -22643,8 +22708,13 @@ var AllowedAddressesSchema = object({
22643
22708
  */
22644
22709
  addresses: array(string()).readonly() });
22645
22710
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
22646
- /** Local hub HTTP port to use in base URLs. */
22647
- port: number().int().min(1).max(65535),
22711
+ /**
22712
+ * LEGACY HINT — do not send from new code. Kept optional so clients
22713
+ * written against the echoing contract keep working; the hub uses it
22714
+ * only when it cannot read its own port, and says so via
22715
+ * `localPort.source === 'caller-hint'`.
22716
+ */
22717
+ port: number().int().min(1).max(65535).optional(),
22648
22718
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
22649
22719
  * candidate. Default `true`. */
22650
22720
  includeLoopback: boolean().optional(),
@@ -34144,6 +34214,383 @@ function removeCustomModel(list, modelId) {
34144
34214
  removed: next.length !== list.length
34145
34215
  };
34146
34216
  }
34217
+ var FRIGATE_PUBLIC_CATALOG = [{
34218
+ id: "frigate-mobiledet-edgetpu",
34219
+ label: "Frigate default — SSDLite MobileDet (EdgeTPU)",
34220
+ format: "tflite",
34221
+ url: "https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess_edgetpu.tflite",
34222
+ sizeMB: 5.4,
34223
+ inputSize: 320,
34224
+ postprocessor: "ssd",
34225
+ labels: [
34226
+ "person",
34227
+ "bicycle",
34228
+ "car",
34229
+ "motorcycle",
34230
+ "airplane",
34231
+ "bus",
34232
+ "train",
34233
+ "car",
34234
+ "boat",
34235
+ "traffic light",
34236
+ "fire hydrant",
34237
+ "street sign",
34238
+ "stop sign",
34239
+ "parking meter",
34240
+ "bench",
34241
+ "bird",
34242
+ "cat",
34243
+ "dog",
34244
+ "horse",
34245
+ "sheep",
34246
+ "cow",
34247
+ "elephant",
34248
+ "bear",
34249
+ "zebra",
34250
+ "giraffe",
34251
+ "hat",
34252
+ "backpack",
34253
+ "umbrella",
34254
+ "shoe",
34255
+ "eye glasses",
34256
+ "handbag",
34257
+ "tie",
34258
+ "suitcase",
34259
+ "frisbee",
34260
+ "skis",
34261
+ "snowboard",
34262
+ "sports ball",
34263
+ "kite",
34264
+ "baseball bat",
34265
+ "baseball glove",
34266
+ "skateboard",
34267
+ "surfboard",
34268
+ "tennis racket",
34269
+ "bottle",
34270
+ "plate",
34271
+ "wine glass",
34272
+ "cup",
34273
+ "fork",
34274
+ "knife",
34275
+ "spoon",
34276
+ "bowl",
34277
+ "banana",
34278
+ "apple",
34279
+ "sandwich",
34280
+ "orange",
34281
+ "broccoli",
34282
+ "carrot",
34283
+ "hot dog",
34284
+ "pizza",
34285
+ "donut",
34286
+ "cake",
34287
+ "chair",
34288
+ "couch",
34289
+ "potted plant",
34290
+ "bed",
34291
+ "mirror",
34292
+ "dining table",
34293
+ "window",
34294
+ "desk",
34295
+ "toilet",
34296
+ "door",
34297
+ "tv",
34298
+ "laptop",
34299
+ "mouse",
34300
+ "remote",
34301
+ "keyboard",
34302
+ "cell phone",
34303
+ "microwave",
34304
+ "oven",
34305
+ "toaster",
34306
+ "sink",
34307
+ "refrigerator",
34308
+ "blender",
34309
+ "book",
34310
+ "clock",
34311
+ "vase",
34312
+ "scissors",
34313
+ "teddy bear",
34314
+ "hair drier",
34315
+ "toothbrush",
34316
+ "hair brush"
34317
+ ],
34318
+ notes: "Frigate's default detection model (the one a stock Frigate + Coral install runs). EdgeTPU-compiled full-integer TFLite, 320×320, TFLite_Detection_PostProcess output → ssd decode. Requires a Coral (tflite format) node."
34319
+ }];
34320
+ /**
34321
+ * Build the registrable descriptor for a curated catalog entry. The URL is
34322
+ * the PUBLIC one — any node downloads it on demand; no Distribute needed.
34323
+ */
34324
+ function buildFrigateCatalogDescriptor(catalogId, overrides) {
34325
+ const cat = FRIGATE_PUBLIC_CATALOG.find((e) => e.id === catalogId);
34326
+ if (!cat) throw new Error(`Unknown Frigate catalog id "${catalogId}" — available: ${FRIGATE_PUBLIC_CATALOG.map((e) => e.id).join(", ")}`);
34327
+ return {
34328
+ stepId: "object-detection",
34329
+ entry: {
34330
+ id: overrides?.modelId?.trim() || cat.id,
34331
+ name: overrides?.name?.trim() || cat.label,
34332
+ description: `${cat.notes} Imported from the Frigate public catalog via Model Studio.`,
34333
+ formats: { [cat.format]: {
34334
+ url: cat.url,
34335
+ sizeMB: cat.sizeMB,
34336
+ runtimes: ["python"]
34337
+ } },
34338
+ inputSize: {
34339
+ width: cat.inputSize,
34340
+ height: cat.inputSize
34341
+ },
34342
+ labels: cat.labels.map((name) => ({
34343
+ id: name,
34344
+ name
34345
+ })),
34346
+ preprocessMode: "letterbox",
34347
+ ...cat.postprocessor !== void 0 ? { postprocessor: cat.postprocessor } : {}
34348
+ }
34349
+ };
34350
+ }
34351
+ //#endregion
34352
+ //#region src/frigate/plus-conversion.ts
34353
+ /** Converted formats ADD to the imported entry; identity fields (decode,
34354
+ * labels, description, preprocess) stay the import's. */
34355
+ function mergeConvertedFormats(base, converted) {
34356
+ return {
34357
+ ...base,
34358
+ formats: {
34359
+ ...base.formats,
34360
+ ...converted.formats
34361
+ }
34362
+ };
34363
+ }
34364
+ function targetOf(format) {
34365
+ return format === "openvino" ? {
34366
+ format,
34367
+ precisions: ["fp16"]
34368
+ } : { format };
34369
+ }
34370
+ /**
34371
+ * Convert the imported ONNX for each requested engine, one target at a time,
34372
+ * merging every success into the registered entry. Returns one outcome per
34373
+ * requested format — the caller surfaces them verbatim.
34374
+ */
34375
+ async function runPlusConversions(deps, args) {
34376
+ const outcomes = [];
34377
+ let current = args.entry;
34378
+ for (const format of args.targetFormats) {
34379
+ const targets = [targetOf(format)];
34380
+ try {
34381
+ const ranked = await deps.recommend({ targets });
34382
+ const best = ranked.find((r) => r.supported);
34383
+ if (!best) {
34384
+ const reason = ranked[0]?.reason ?? "no node can satisfy this target";
34385
+ outcomes.push({
34386
+ format,
34387
+ ok: false,
34388
+ error: reason
34389
+ });
34390
+ continue;
34391
+ }
34392
+ const sourceUrl = await deps.freshSourceUrl();
34393
+ const result = await deps.convert({
34394
+ sourceUrl,
34395
+ metadata: {
34396
+ id: current.id,
34397
+ name: current.name,
34398
+ labels: current.labels,
34399
+ inputSize: current.inputSize,
34400
+ preprocessMode: current.preprocessMode ?? "letterbox",
34401
+ outputFormat: "yolo"
34402
+ },
34403
+ targets
34404
+ }, best.nodeId);
34405
+ if (result.artifacts.length === 0) {
34406
+ outcomes.push({
34407
+ format,
34408
+ ok: false,
34409
+ nodeId: best.nodeId,
34410
+ error: "no validated artifacts"
34411
+ });
34412
+ continue;
34413
+ }
34414
+ current = mergeConvertedFormats(current, result.entry);
34415
+ await deps.upsertEntry(current);
34416
+ await deps.markAvailable(best.nodeId, current.id, result.artifacts);
34417
+ outcomes.push({
34418
+ format,
34419
+ ok: true,
34420
+ nodeId: best.nodeId
34421
+ });
34422
+ } catch (err) {
34423
+ outcomes.push({
34424
+ format,
34425
+ ok: false,
34426
+ error: err instanceof Error ? err.message : "conversion failed"
34427
+ });
34428
+ }
34429
+ }
34430
+ return outcomes;
34431
+ }
34432
+ //#endregion
34433
+ //#region src/frigate/frigate-plus.ts
34434
+ var PLUS_API_HOST = "https://api.frigate.video";
34435
+ var PLUS_KEY_RE = /^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}:[a-z0-9]{40}$/;
34436
+ function isValidPlusApiKey(key) {
34437
+ return PLUS_KEY_RE.test(key);
34438
+ }
34439
+ function isRecord(value) {
34440
+ return typeof value === "object" && value !== null && !Array.isArray(value);
34441
+ }
34442
+ /** Exchange the API key for an access token. The key never appears in errors. */
34443
+ async function fetchPlusAccessToken(fetchFn, apiKey) {
34444
+ if (!isValidPlusApiKey(apiKey)) throw new Error("Frigate+ API key is malformed — expected the \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:40-hex\" shape shown in your Frigate+ account settings");
34445
+ const [user, pass] = apiKey.split(":");
34446
+ const basic = Buffer.from(`${user}:${pass}`).toString("base64");
34447
+ const res = await fetchFn(`${PLUS_API_HOST}/v1/auth/token`, { headers: { authorization: `Basic ${basic}` } });
34448
+ if (!res.ok) throw new Error(`Frigate+ authentication failed (HTTP ${res.status}) — check the API key in the Model Studio addon settings`);
34449
+ const body = await res.json();
34450
+ const token = isRecord(body) && typeof body.accessToken === "string" ? body.accessToken : null;
34451
+ if (token === null) throw new Error("Frigate+ auth response carried no accessToken");
34452
+ return token;
34453
+ }
34454
+ async function plusGet(fetchFn, token, path) {
34455
+ const res = await fetchFn(`${PLUS_API_HOST}/v1/${path}`, { headers: { authorization: `Bearer ${token}` } });
34456
+ if (!res.ok) throw new Error(`Frigate+ request "${path}" failed (HTTP ${res.status}): ${await res.text()}`);
34457
+ return res.json();
34458
+ }
34459
+ /**
34460
+ * `verifiedCounts` has THREE real shapes and none may break the parse:
34461
+ * -1 → base model ("not applicable") ⇒ 0
34462
+ * { "11": 199 } → legacy custom (flat label→count)
34463
+ * { camera: { label: n, … }, … } → recent custom (per camera)
34464
+ */
34465
+ function verifiedImageCount(raw) {
34466
+ if (typeof raw === "number") return raw > 0 ? raw : 0;
34467
+ if (!isRecord(raw)) return 0;
34468
+ let total = 0;
34469
+ for (const value of Object.values(raw)) if (typeof value === "number") total += value > 0 ? value : 0;
34470
+ else if (isRecord(value)) {
34471
+ for (const inner of Object.values(value)) if (typeof inner === "number" && inner > 0) total += inner;
34472
+ }
34473
+ return total;
34474
+ }
34475
+ function toModelSummary(m) {
34476
+ if (typeof m.id !== "string") return null;
34477
+ return {
34478
+ id: m.id,
34479
+ name: typeof m.name === "string" ? m.name : m.id,
34480
+ type: typeof m.type === "string" ? m.type : "unknown",
34481
+ isBaseModel: m.isBaseModel === true,
34482
+ baseModel: typeof m.baseModel === "string" ? m.baseModel : "",
34483
+ supportedDetectors: Array.isArray(m.supportedDetectors) ? m.supportedDetectors.filter((d) => typeof d === "string") : [],
34484
+ trainDate: typeof m.trainDate === "string" ? m.trainDate : "",
34485
+ ...typeof m.hailoDevice === "string" ? { hailoDevice: m.hailoDevice } : {},
34486
+ verifiedImages: verifiedImageCount(m.verifiedCounts),
34487
+ ...typeof m.width === "number" ? { width: m.width } : {},
34488
+ ...typeof m.height === "number" ? { height: m.height } : {}
34489
+ };
34490
+ }
34491
+ /** Parse a raw `/v1/model/list` body (`{list:[…]}`, or a bare array). */
34492
+ function toPlusModelSummaries(raw) {
34493
+ return (Array.isArray(raw) ? raw : isRecord(raw) && Array.isArray(raw.list) ? raw.list : []).filter(isRecord).map(toModelSummary).filter((m) => m !== null);
34494
+ }
34495
+ /**
34496
+ * EVERYTHING the account has access to — base AND custom models. The real
34497
+ * response wraps the array: `{ "list": [...] }` (a bare array is tolerated
34498
+ * for forward compatibility).
34499
+ */
34500
+ async function listPlusModels(fetchFn, apiKey) {
34501
+ return toPlusModelSummaries(await plusGet(fetchFn, await fetchPlusAccessToken(fetchFn, apiKey), "model/list"));
34502
+ }
34503
+ function toModelInfo(raw, modelId) {
34504
+ if (!isRecord(raw)) throw new Error("Frigate+ model info: unexpected response shape");
34505
+ const labelMap = {};
34506
+ if (isRecord(raw.labelMap)) {
34507
+ for (const [k, v] of Object.entries(raw.labelMap)) if (typeof v === "string") labelMap[k] = v;
34508
+ }
34509
+ return {
34510
+ id: typeof raw.id === "string" ? raw.id : modelId,
34511
+ type: typeof raw.type === "string" ? raw.type : "unknown",
34512
+ width: typeof raw.width === "number" ? raw.width : 320,
34513
+ height: typeof raw.height === "number" ? raw.height : 320,
34514
+ labelMap
34515
+ };
34516
+ }
34517
+ /** Resolve one model's metadata + presigned artifact URL. */
34518
+ async function requestPlusModelDownload(fetchFn, apiKey, plusModelId) {
34519
+ const token = await fetchPlusAccessToken(fetchFn, apiKey);
34520
+ const info = toModelInfo(await plusGet(fetchFn, token, `model/${plusModelId}`), plusModelId);
34521
+ const signed = await plusGet(fetchFn, token, `model/${plusModelId}/signed_url`);
34522
+ const url = isRecord(signed) && typeof signed.url === "string" ? signed.url : null;
34523
+ if (url === null) throw new Error("Frigate+ signed_url response carried no url");
34524
+ return {
34525
+ info,
34526
+ downloadUrl: url
34527
+ };
34528
+ }
34529
+ /** Frigate+ model type → our format + decode. Mirrors the Frigate presets. */
34530
+ var PLUS_TYPE_MAP = {
34531
+ mobiledet: {
34532
+ format: "tflite",
34533
+ postprocessor: "ssd",
34534
+ extension: ".tflite"
34535
+ },
34536
+ yolonas: {
34537
+ format: "onnx",
34538
+ postprocessor: "yolonas",
34539
+ extension: ".onnx"
34540
+ },
34541
+ yolov9: {
34542
+ format: "onnx",
34543
+ extension: ".onnx"
34544
+ }
34545
+ };
34546
+ function plusTypeTraits(type) {
34547
+ return PLUS_TYPE_MAP[type] ?? {
34548
+ format: "onnx",
34549
+ extension: ".onnx"
34550
+ };
34551
+ }
34552
+ /** Ordered labels from the API's `index → label` map; holes keep alignment. */
34553
+ function orderedLabels(labelMap) {
34554
+ const indices = Object.keys(labelMap).map((k) => Number.parseInt(k, 10)).filter((n) => Number.isInteger(n) && n >= 0);
34555
+ if (indices.length === 0) return [];
34556
+ const max = Math.max(...indices);
34557
+ const out = [];
34558
+ for (let i = 0; i <= max; i++) out.push(labelMap[String(i)] ?? `unknown-${i}`);
34559
+ return out;
34560
+ }
34561
+ /**
34562
+ * Build the registrable descriptor for a downloaded Frigate+ model. The
34563
+ * artifact is hub-local (landed by the server-side download), so the URL uses
34564
+ * the `camstack-local://` convention (D208) — other nodes get it via
34565
+ * Distribute.
34566
+ */
34567
+ function plusInfoToDescriptor(info, opts) {
34568
+ const traits = plusTypeTraits(info.type);
34569
+ const labels = orderedLabels(info.labelMap);
34570
+ return {
34571
+ stepId: "object-detection",
34572
+ entry: {
34573
+ id: opts.modelId,
34574
+ name: opts.name,
34575
+ description: `Frigate+ personal model (${info.type}) — imported via Model Studio`,
34576
+ formats: { [traits.format]: {
34577
+ url: `camstack-local://${opts.modelId}/${opts.filename}`,
34578
+ sizeMB: opts.sizeMB,
34579
+ runtimes: ["python"]
34580
+ } },
34581
+ inputSize: {
34582
+ width: info.width,
34583
+ height: info.height
34584
+ },
34585
+ labels: labels.map((name) => ({
34586
+ id: name,
34587
+ name
34588
+ })),
34589
+ preprocessMode: "letterbox",
34590
+ ...traits.postprocessor !== void 0 ? { postprocessor: traits.postprocessor } : {}
34591
+ }
34592
+ };
34593
+ }
34147
34594
  var MODEL_UPLOAD_ALLOWED_EXTENSIONS = [".onnx", ".tflite"];
34148
34595
  var FILENAME_RE = /^[a-zA-Z0-9._-]+$/;
34149
34596
  /**
@@ -34285,6 +34732,78 @@ var modelStudioActions = defineCustomActions({
34285
34732
  }), { kind: "mutation" }),
34286
34733
  abortModelUpload: customAction(object({ uploadId: string() }), object({ ok: literal(true) }), { kind: "mutation" }),
34287
34734
  /**
34735
+ * Curated PUBLIC Frigate model catalog (D214 addendum) — models Frigate
34736
+ * uses/references that are downloadable without credentials, with verified
34737
+ * metadata so the operator types nothing. Import downloads SERVER-SIDE into
34738
+ * the hub's modelsDir and registers the descriptor with the public URL
34739
+ * (other nodes download on demand — no Distribute needed).
34740
+ */
34741
+ listFrigateCatalog: customAction(_void(), array(object({
34742
+ id: string(),
34743
+ label: string(),
34744
+ format: _enum(MODEL_FORMATS),
34745
+ sizeMB: number(),
34746
+ inputSize: number(),
34747
+ notes: string()
34748
+ })).readonly()),
34749
+ importFrigateCatalogModel: customAction(object({
34750
+ catalogId: string(),
34751
+ modelId: string().optional(),
34752
+ name: string().optional()
34753
+ }), object({
34754
+ ok: literal(true),
34755
+ modelId: string(),
34756
+ bytes: number()
34757
+ }), { kind: "mutation" }),
34758
+ /**
34759
+ * Frigate+ personal models. The API key lives in the addon's SETTINGS
34760
+ * (password field) and never leaves the hub: list / test / import all run
34761
+ * server-side against api.frigate.video. Status deliberately reveals only
34762
+ * booleans — never the key.
34763
+ */
34764
+ /**
34765
+ * Nodes whose engine can actually LOAD a tflite artifact (the pool's only
34766
+ * tflite runtime binds the Coral EdgeTPU delegate — no Coral, no tflite).
34767
+ * The UI gates tflite imports on this instead of letting them fail at load.
34768
+ */
34769
+ tfliteSupport: customAction(_void(), object({ nodes: array(string()).readonly() })),
34770
+ frigatePlusStatus: customAction(_void(), object({
34771
+ configured: boolean(),
34772
+ keyFormatValid: boolean()
34773
+ })),
34774
+ frigatePlusTest: customAction(_void(), object({
34775
+ ok: boolean(),
34776
+ error: string().optional()
34777
+ }), { kind: "mutation" }),
34778
+ frigatePlusListModels: customAction(_void(), array(object({
34779
+ id: string(),
34780
+ name: string(),
34781
+ type: string(),
34782
+ isBaseModel: boolean(),
34783
+ baseModel: string(),
34784
+ supportedDetectors: array(string()).readonly(),
34785
+ trainDate: string(),
34786
+ hailoDevice: string().optional(),
34787
+ verifiedImages: number(),
34788
+ width: number().optional(),
34789
+ height: number().optional()
34790
+ })).readonly()),
34791
+ importFrigatePlusModel: customAction(object({
34792
+ plusModelId: string(),
34793
+ modelId: string(),
34794
+ name: string().optional()
34795
+ }), object({
34796
+ ok: literal(true),
34797
+ modelId: string(),
34798
+ bytes: number(),
34799
+ conversions: array(object({
34800
+ format: _enum(["openvino", "coreml"]),
34801
+ ok: boolean(),
34802
+ nodeId: string().optional(),
34803
+ error: string().optional()
34804
+ })).readonly()
34805
+ }), { kind: "mutation" }),
34806
+ /**
34288
34807
  * Rank nodes for model conversion based on hardware capabilities.
34289
34808
  * CoreML targets require darwin; OpenVINO targets require an OpenVINO-capable node.
34290
34809
  */
@@ -34367,18 +34886,9 @@ function spawnRunConvert(jobPath, pythonPath, onProgress, _sessionId) {
34367
34886
  });
34368
34887
  });
34369
34888
  }
34370
- /**
34371
- * Model Studio provides the `custom-model-registry` collection cap (hub only)
34372
- * and the `model-convert` singleton cap (any node).
34373
- *
34374
- * P1 maintained the durable list of custom detection models and exposed them
34375
- * so the detection pipeline can offer them in its model picker.
34376
- *
34377
- * P3 adds the `model-convert` provider on EVERY node so agents can run
34378
- * local Python conversion without routing through the hub.
34379
- * The `custom-model-registry` cap and all customActions are hub-only.
34380
- */
34381
- var ModelStudioAddon = class extends BaseAddon {
34889
+ /** Global-fetch adapter for the injected Frigate+ client seam. */
34890
+ var plusFetch = async (url, init) => fetch(url, init);
34891
+ var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
34382
34892
  id = "model-studio";
34383
34893
  modelsState = null;
34384
34894
  availabilityState = null;
@@ -34400,7 +34910,21 @@ var ModelStudioAddon = class extends BaseAddon {
34400
34910
  section: "detection"
34401
34911
  }];
34402
34912
  constructor() {
34403
- super({});
34913
+ super({ frigatePlusApiKey: "" });
34914
+ }
34915
+ globalSettingsSchema() {
34916
+ return this.schema({ sections: [{
34917
+ id: "frigate-plus",
34918
+ title: "Frigate+",
34919
+ description: "Credentials for importing your personal Frigate+ models. Create an API key in your Frigate+ account (https://plus.frigate.video → Settings → API keys; see docs.frigate.video/integrations/plus). The key is used only server-side against api.frigate.video and is never sent to the browser.",
34920
+ fields: [{
34921
+ type: "password",
34922
+ key: "frigatePlusApiKey",
34923
+ label: "Frigate+ API key",
34924
+ description: "Shape: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:40-hex-chars",
34925
+ showToggle: true
34926
+ }]
34927
+ }] });
34404
34928
  }
34405
34929
  makeConvertDeps() {
34406
34930
  return {
@@ -34487,6 +35011,45 @@ var ModelStudioAddon = class extends BaseAddon {
34487
35011
  listCustomModels: async () => this.listCustomModels(),
34488
35012
  distributeModel: async (input) => this.distributeModel(input),
34489
35013
  listAvailability: async () => this.availability_.get(),
35014
+ listFrigateCatalog: async () => FRIGATE_PUBLIC_CATALOG.map((c) => ({
35015
+ id: c.id,
35016
+ label: c.label,
35017
+ format: c.format,
35018
+ sizeMB: c.sizeMB,
35019
+ inputSize: c.inputSize,
35020
+ notes: c.notes
35021
+ })),
35022
+ importFrigateCatalogModel: async (input) => this.importFrigateCatalogModel(input),
35023
+ tfliteSupport: async () => {
35024
+ const api = this.ctx.api;
35025
+ if (!api) return { nodes: [] };
35026
+ const nodes = await this.recommendDeps().listNodes();
35027
+ const supported = [];
35028
+ for (const nId of nodes) try {
35029
+ if ((await api.platformProbe.getCapabilities.query({ nodeId: nId })).scores.some((sc) => sc.format === "tflite" && sc.available)) supported.push(nId);
35030
+ } catch {}
35031
+ return { nodes: supported };
35032
+ },
35033
+ frigatePlusStatus: async () => {
35034
+ const key = this.config.frigatePlusApiKey ?? "";
35035
+ return {
35036
+ configured: key.length > 0,
35037
+ keyFormatValid: isValidPlusApiKey(key)
35038
+ };
35039
+ },
35040
+ frigatePlusTest: async () => {
35041
+ try {
35042
+ await fetchPlusAccessToken(plusFetch, this.config.frigatePlusApiKey ?? "");
35043
+ return { ok: true };
35044
+ } catch (err) {
35045
+ return {
35046
+ ok: false,
35047
+ error: err instanceof Error ? err.message : "auth failed"
35048
+ };
35049
+ }
35050
+ },
35051
+ frigatePlusListModels: async () => listPlusModels(plusFetch, this.config.frigatePlusApiKey ?? ""),
35052
+ importFrigatePlusModel: async (input) => this.importFrigatePlusModel(input),
34490
35053
  recommendConvertNode: async (input) => {
34491
35054
  const api = this.ctx.api;
34492
35055
  if (!api) throw new Error("recommendConvertNode: ctx.api unavailable");
@@ -34616,6 +35179,170 @@ var ModelStudioAddon = class extends BaseAddon {
34616
35179
  bytes: res.bytes
34617
35180
  };
34618
35181
  }
35182
+ /** listNodes/hardwareOf deps for `recommendConvertNode` — shared by the
35183
+ * convert actions and the Frigate+ auto-conversion. */
35184
+ recommendDeps() {
35185
+ const api = this.ctx.api;
35186
+ if (!api) throw new Error("model-studio: ctx.api unavailable");
35187
+ return {
35188
+ listNodes: async () => {
35189
+ return (await api.addons.listCapabilityProviders.query({ capName: "model-convert" })).map((p) => {
35190
+ const atIdx = p.addonId.indexOf("@");
35191
+ return atIdx !== -1 ? p.addonId.slice(atIdx + 1) : "hub";
35192
+ });
35193
+ },
35194
+ hardwareOf: async (nId) => {
35195
+ const hw = await api.platformProbe.getHardware.query({ nodeId: nId });
35196
+ const caps = await api.platformProbe.getCapabilities.query({ nodeId: nId });
35197
+ return {
35198
+ platform: hw.platform,
35199
+ gpu: hw.gpu ?? void 0,
35200
+ npu: hw.npu ?? void 0,
35201
+ openvino: caps.scores.some((s) => s.format === "openvino" && s.available)
35202
+ };
35203
+ }
35204
+ };
35205
+ }
35206
+ /** sha256 of a file on disk (streamed). */
35207
+ static async fileSha256(filePath) {
35208
+ const hash = crypto$1.createHash("sha256");
35209
+ await new Promise((resolve, reject) => {
35210
+ fs.createReadStream(filePath).on("data", (chunk) => hash.update(chunk)).on("end", () => resolve()).on("error", reject);
35211
+ });
35212
+ return hash.digest("hex");
35213
+ }
35214
+ /**
35215
+ * Import a curated PUBLIC Frigate catalog model: download SERVER-SIDE into
35216
+ * the hub's modelsDir (size-verified against the curated declaration),
35217
+ * register the descriptor with the PUBLIC url (nodes download on demand),
35218
+ * and record hub availability.
35219
+ */
35220
+ async importFrigateCatalogModel(input) {
35221
+ const cat = FRIGATE_PUBLIC_CATALOG.find((c) => c.id === input.catalogId);
35222
+ if (!cat) throw new Error(`importFrigateCatalogModel: unknown catalog id "${input.catalogId}"`);
35223
+ const descriptor = buildFrigateCatalogDescriptor(input.catalogId, {
35224
+ ...input.modelId !== void 0 ? { modelId: input.modelId } : {},
35225
+ ...input.name !== void 0 ? { name: input.name } : {}
35226
+ });
35227
+ const modelsDir = await this.resolveModelsDir();
35228
+ const filename = cat.url.split("/").at(-1) ?? `${descriptor.entry.id}.${cat.format}`;
35229
+ const destPath = path.join(modelsDir, filename);
35230
+ await downloadFile(cat.url, destPath);
35231
+ const bytes = (await fs.promises.stat(destPath)).size;
35232
+ const declaredBytes = cat.sizeMB * 1e6;
35233
+ if (bytes < declaredBytes * .5 || bytes > declaredBytes * 2) {
35234
+ await fs.promises.rm(destPath, { force: true });
35235
+ throw new Error(`importFrigateCatalogModel: downloaded ${bytes} bytes but the catalog declares ~${Math.round(declaredBytes)} — refusing to register a suspicious artifact`);
35236
+ }
35237
+ const sha256 = await ModelStudioAddon.fileSha256(destPath);
35238
+ await this.registerCustomModel(descriptor);
35239
+ await this.availability_.update((prev) => upsertAvailability(prev, descriptor.entry.id, "hub", {
35240
+ format: cat.format,
35241
+ sha256,
35242
+ bytes,
35243
+ at: Date.now()
35244
+ }));
35245
+ this.ctx.logger.info("Imported Frigate catalog model", { meta: {
35246
+ catalogId: input.catalogId,
35247
+ modelId: descriptor.entry.id,
35248
+ bytes,
35249
+ sha256
35250
+ } });
35251
+ return {
35252
+ ok: true,
35253
+ modelId: descriptor.entry.id,
35254
+ bytes
35255
+ };
35256
+ }
35257
+ /**
35258
+ * Import one of the operator's personal Frigate+ models: resolve metadata +
35259
+ * presigned URL with the SECRET key (server-side only), download into the
35260
+ * hub's modelsDir, and register with the model-type-derived characteristics
35261
+ * (format, decode, input size, labelmap — nothing hand-typed). The artifact
35262
+ * is hub-local: other nodes get it via Models → Distribute.
35263
+ */
35264
+ async importFrigatePlusModel(input) {
35265
+ if (!/^[a-zA-Z0-9._-]+$/.test(input.modelId)) throw new Error(`importFrigatePlusModel: model id "${input.modelId}" is invalid`);
35266
+ const key = this.config.frigatePlusApiKey ?? "";
35267
+ const { info, downloadUrl } = await requestPlusModelDownload(plusFetch, key, input.plusModelId);
35268
+ const traits = plusTypeTraits(info.type);
35269
+ const filename = `${input.modelId}${traits.extension}`;
35270
+ const modelsDir = await this.resolveModelsDir();
35271
+ const destPath = path.join(modelsDir, filename);
35272
+ await downloadFile(downloadUrl, destPath);
35273
+ const bytes = (await fs.promises.stat(destPath)).size;
35274
+ if (bytes < 1e4) {
35275
+ await fs.promises.rm(destPath, { force: true });
35276
+ throw new Error(`importFrigatePlusModel: downloaded only ${bytes} bytes — the signed URL likely expired or the response was an error page`);
35277
+ }
35278
+ const sha256 = await ModelStudioAddon.fileSha256(destPath);
35279
+ const descriptor = plusInfoToDescriptor(info, {
35280
+ modelId: input.modelId,
35281
+ name: input.name?.trim() || `Frigate+ ${info.type} (${input.plusModelId.slice(0, 8)})`,
35282
+ filename,
35283
+ sizeMB: Number((bytes / 1e6).toFixed(1))
35284
+ });
35285
+ await this.registerCustomModel(descriptor);
35286
+ await this.availability_.update((prev) => upsertAvailability(prev, input.modelId, "hub", {
35287
+ format: traits.format,
35288
+ sha256,
35289
+ bytes,
35290
+ at: Date.now()
35291
+ }));
35292
+ this.ctx.logger.info("Imported Frigate+ model", { meta: {
35293
+ plusModelId: input.plusModelId,
35294
+ modelId: input.modelId,
35295
+ type: info.type,
35296
+ bytes
35297
+ } });
35298
+ let conversions = [];
35299
+ if (traits.format === "onnx") {
35300
+ const api = this.ctx.api;
35301
+ if (api) {
35302
+ conversions = await runPlusConversions({
35303
+ recommend: (i) => recommendConvertNode(i, this.recommendDeps()),
35304
+ convert: (convertInput, nId) => api.modelConvert.convert.mutate(convertInput, nodePin(nId)),
35305
+ freshSourceUrl: async () => (await requestPlusModelDownload(plusFetch, key, input.plusModelId)).downloadUrl,
35306
+ upsertEntry: async (entry) => {
35307
+ await this.state_.update((prev) => upsertCustomModel(prev, {
35308
+ stepId: descriptor.stepId,
35309
+ entry
35310
+ }));
35311
+ },
35312
+ markAvailable: async (nId, mId, artifacts) => {
35313
+ await this.availability_.update((prev) => {
35314
+ const seen = /* @__PURE__ */ new Set();
35315
+ let next = prev;
35316
+ for (const artifact of artifacts) {
35317
+ if (seen.has(artifact.format)) continue;
35318
+ seen.add(artifact.format);
35319
+ next = upsertAvailability(next, mId, nId, {
35320
+ format: artifact.format,
35321
+ sha256: "",
35322
+ bytes: Math.round(artifact.sizeMB * 1e6),
35323
+ at: Date.now()
35324
+ });
35325
+ }
35326
+ return next;
35327
+ });
35328
+ }
35329
+ }, {
35330
+ entry: descriptor.entry,
35331
+ targetFormats: ["openvino", "coreml"]
35332
+ });
35333
+ this.ctx.logger.info("Frigate+ import — engine conversions", { meta: {
35334
+ modelId: input.modelId,
35335
+ outcomes: conversions.map((o) => ({ ...o }))
35336
+ } });
35337
+ }
35338
+ }
35339
+ return {
35340
+ ok: true,
35341
+ modelId: input.modelId,
35342
+ bytes,
35343
+ conversions
35344
+ };
35345
+ }
34619
35346
  async registerCustomModel(input) {
34620
35347
  if (!input.stepId.trim()) throw new Error("registerCustomModel: stepId is required");
34621
35348
  if (!input.entry.id.trim()) throw new Error("registerCustomModel: entry.id is required");