@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.
- package/dist/{MotionZonesSettings-sZkppmGH.mjs → MotionZonesSettings-W4fQr3y3.mjs} +2 -2
- package/dist/{PrivacyMaskSettings-C4IQ_tob.mjs → PrivacyMaskSettings-CtKzQjYD.mjs} +4 -4
- package/dist/{SceneMonitorEditor-Dv55kWSO.mjs → SceneMonitorEditor-CM3z_N4Q.mjs} +3 -3
- package/dist/_stub.js +3757 -3212
- 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
- package/dist/_virtual_mf___mfe_internal__addon_model_studio_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Boi8PUUw.mjs +26 -0
- package/dist/addon-model-studio.css +1 -1
- package/dist/{hostInit-DXmPMLUp.mjs → hostInit-BKRnEFn7.mjs} +3 -3
- package/dist/model-studio.addon.js +746 -19
- package/dist/model-studio.addon.mjs +746 -19
- package/dist/{player-overlays-CS5IM4vB.mjs → player-overlays-CNkUzVZI.mjs} +1 -1
- package/dist/remoteEntry.js +1 -1
- package/dist/{responsive-CWtuZhDz.mjs → responsive-D-JIMJTX.mjs} +1 -1
- package/dist/{square-OXaWZ-Np.mjs → square-CBwtzgNH.mjs} +1 -1
- package/dist/{trash-2-BtfCrPnQ.mjs → trash-2-CiJaurwE.mjs} +1 -1
- package/dist/{use-device-snapshot-Bx87WhSL.mjs → use-device-snapshot-DpcJhtJX.mjs} +1 -1
- 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
- package/package.json +1 -1
- package/dist/_virtual_mf___mfe_internal__addon_model_studio_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Do4Kqwhj.mjs +0 -26
|
@@ -90,7 +90,7 @@ async function downloadFile(url, destPath, onProgress) {
|
|
|
90
90
|
(0, node_util.promisify)(node_zlib.brotliCompress);
|
|
91
91
|
(0, node_util.promisify)(node_zlib.gzip);
|
|
92
92
|
//#endregion
|
|
93
|
-
//#region ../types/dist/event-category-
|
|
93
|
+
//#region ../types/dist/event-category-CIa_iT6b.mjs
|
|
94
94
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
95
95
|
EventCategory["SystemBoot"] = "system.boot";
|
|
96
96
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -106,6 +106,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
106
106
|
*/
|
|
107
107
|
EventCategory["SystemRestartCompleted"] = "system.restart-completed";
|
|
108
108
|
/**
|
|
109
|
+
* The hub reissued its own TLS certificate at boot (`ensureTlsCert`).
|
|
110
|
+
* Emitted only when the material on disk actually changed, so an
|
|
111
|
+
* operator who trusted the old certificate by hand is told rather than
|
|
112
|
+
* discovering it as a browser error. Payload `TlsCertChangedPayload`.
|
|
113
|
+
*
|
|
114
|
+
* Rule: docs/decisions/adr-0227-*.md
|
|
115
|
+
*/
|
|
116
|
+
EventCategory["SystemTlsCertChanged"] = "system.tls-cert-changed";
|
|
117
|
+
/**
|
|
109
118
|
* A newer addon or server-root package version was found by the
|
|
110
119
|
* authoritative registry check. Emitted once when any observed
|
|
111
120
|
* `latestVersion` changes (or a package/node first appears behind);
|
|
@@ -18814,6 +18823,12 @@ var CameraStatusSchema = object({
|
|
|
18814
18823
|
/** Unix timestamp (ms) when this snapshot was composed server-side. */
|
|
18815
18824
|
fetchedAt: number()
|
|
18816
18825
|
});
|
|
18826
|
+
var InferenceDeviceExclusionReasonSchema = _enum([
|
|
18827
|
+
"disabled",
|
|
18828
|
+
"unavailable",
|
|
18829
|
+
"cannot-host-camera-root",
|
|
18830
|
+
"accelerator-preferred"
|
|
18831
|
+
]);
|
|
18817
18832
|
var NodeInferenceDeviceSchema = object({
|
|
18818
18833
|
/** Stable per-node device key, e.g. `openvino:npu`, `edgetpu:usb`, `cpu`. */
|
|
18819
18834
|
key: string(),
|
|
@@ -18844,7 +18859,17 @@ var NodeInferenceDeviceSchema = object({
|
|
|
18844
18859
|
* available per format; this is the stored selection that becomes the
|
|
18845
18860
|
* default for EVERY camera landing on this accelerator.
|
|
18846
18861
|
*/
|
|
18847
|
-
steps: record(string(), DeviceStepConfigSchema).optional()
|
|
18862
|
+
steps: record(string(), DeviceStepConfigSchema).optional(),
|
|
18863
|
+
/**
|
|
18864
|
+
* `null` when the device IS a camera-root candidate on this node; otherwise
|
|
18865
|
+
* the reason the dispatcher drops it. Computed by the SAME
|
|
18866
|
+
* `resolveInferenceDeviceEligibility` the dispatcher runs, so this view can
|
|
18867
|
+
* never disagree with the election — deriving it in the UI from
|
|
18868
|
+
* `enabled`/`available` would silently miss `cannot-host-camera-root` (needs
|
|
18869
|
+
* the node's model catalog) and `accelerator-preferred` (needs the node-wide
|
|
18870
|
+
* "an accelerator is serving" predicate).
|
|
18871
|
+
*/
|
|
18872
|
+
exclusion: InferenceDeviceExclusionReasonSchema.nullable()
|
|
18848
18873
|
});
|
|
18849
18874
|
var NodeInferenceDevicesSchema = object({
|
|
18850
18875
|
nodeId: string(),
|
|
@@ -22602,7 +22627,12 @@ var ListResultSchema = object({
|
|
|
22602
22627
|
probedAt: number()
|
|
22603
22628
|
});
|
|
22604
22629
|
var PreferredSchema = LocalInterfaceSchema.nullable();
|
|
22605
|
-
|
|
22630
|
+
/**
|
|
22631
|
+
* Candidate base URL for the SDK to race on connect. Order matters —
|
|
22632
|
+
* the SDK should attempt these top-to-bottom with a short per-candidate
|
|
22633
|
+
* timeout (e.g. 1500ms) and cache the winner for the session.
|
|
22634
|
+
*/
|
|
22635
|
+
var ConnectionEndpointSchema = object({
|
|
22606
22636
|
/** Operator-facing label (e.g. "LAN — en0", "Public tunnel"). */
|
|
22607
22637
|
label: string(),
|
|
22608
22638
|
/** Fully-formed base URL with scheme + host + port. */
|
|
@@ -22645,7 +22675,42 @@ var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
|
|
|
22645
22675
|
* ordering between polls.
|
|
22646
22676
|
*/
|
|
22647
22677
|
priority: number()
|
|
22648
|
-
})
|
|
22678
|
+
});
|
|
22679
|
+
/**
|
|
22680
|
+
* Where the advertised local port came from. Ordered most → least
|
|
22681
|
+
* authoritative, and the whole point of returning it: a client must be able to
|
|
22682
|
+
* tell a FACT about the hub's socket from an echo of its own guess.
|
|
22683
|
+
*/
|
|
22684
|
+
var LocalPortSourceEnum = _enum([
|
|
22685
|
+
"server-config",
|
|
22686
|
+
"server-env",
|
|
22687
|
+
"caller-hint",
|
|
22688
|
+
"default"
|
|
22689
|
+
]);
|
|
22690
|
+
/** The port every LAN/loopback `baseUrl` in the same result was built with. */
|
|
22691
|
+
var AdvertisedLocalPortSchema = object({
|
|
22692
|
+
port: number().int().min(1).max(65535),
|
|
22693
|
+
source: LocalPortSourceEnum
|
|
22694
|
+
});
|
|
22695
|
+
var GetConnectionEndpointsResultSchema = object({
|
|
22696
|
+
endpoints: array(ConnectionEndpointSchema).readonly(),
|
|
22697
|
+
/**
|
|
22698
|
+
* The port the hub built the LAN/loopback URLs with, and where that number
|
|
22699
|
+
* came from.
|
|
22700
|
+
*
|
|
22701
|
+
* Returned rather than merely applied, because "the URL is right" and "the
|
|
22702
|
+
* client can KNOW the URL is right" are different properties. A client that
|
|
22703
|
+
* only sees a corrected URL cannot distinguish a hub that fixed the port from
|
|
22704
|
+
* a hub that echoed the port the client sent, so it cannot decide whether to
|
|
22705
|
+
* race the candidate or discard it. With `source` it can: anything but
|
|
22706
|
+
* `caller-hint` is the hub's own socket.
|
|
22707
|
+
*
|
|
22708
|
+
* Absent on hubs predating this field — a client that finds it missing is
|
|
22709
|
+
* talking to an echoing hub and must degrade exactly as it does for
|
|
22710
|
+
* `caller-hint`.
|
|
22711
|
+
*/
|
|
22712
|
+
localPort: AdvertisedLocalPortSchema
|
|
22713
|
+
});
|
|
22649
22714
|
/**
|
|
22650
22715
|
* The chosen outbound endpoint for notification artifacts. `baseUrl: null` =
|
|
22651
22716
|
* AUTO (resolved from the candidate ranking at send time); `resolved` reports
|
|
@@ -22666,8 +22731,13 @@ var AllowedAddressesSchema = object({
|
|
|
22666
22731
|
*/
|
|
22667
22732
|
addresses: array(string()).readonly() });
|
|
22668
22733
|
method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
|
|
22669
|
-
/**
|
|
22670
|
-
|
|
22734
|
+
/**
|
|
22735
|
+
* LEGACY HINT — do not send from new code. Kept optional so clients
|
|
22736
|
+
* written against the echoing contract keep working; the hub uses it
|
|
22737
|
+
* only when it cannot read its own port, and says so via
|
|
22738
|
+
* `localPort.source === 'caller-hint'`.
|
|
22739
|
+
*/
|
|
22740
|
+
port: number().int().min(1).max(65535).optional(),
|
|
22671
22741
|
/** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
|
|
22672
22742
|
* candidate. Default `true`. */
|
|
22673
22743
|
includeLoopback: boolean().optional(),
|
|
@@ -34167,6 +34237,383 @@ function removeCustomModel(list, modelId) {
|
|
|
34167
34237
|
removed: next.length !== list.length
|
|
34168
34238
|
};
|
|
34169
34239
|
}
|
|
34240
|
+
var FRIGATE_PUBLIC_CATALOG = [{
|
|
34241
|
+
id: "frigate-mobiledet-edgetpu",
|
|
34242
|
+
label: "Frigate default — SSDLite MobileDet (EdgeTPU)",
|
|
34243
|
+
format: "tflite",
|
|
34244
|
+
url: "https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess_edgetpu.tflite",
|
|
34245
|
+
sizeMB: 5.4,
|
|
34246
|
+
inputSize: 320,
|
|
34247
|
+
postprocessor: "ssd",
|
|
34248
|
+
labels: [
|
|
34249
|
+
"person",
|
|
34250
|
+
"bicycle",
|
|
34251
|
+
"car",
|
|
34252
|
+
"motorcycle",
|
|
34253
|
+
"airplane",
|
|
34254
|
+
"bus",
|
|
34255
|
+
"train",
|
|
34256
|
+
"car",
|
|
34257
|
+
"boat",
|
|
34258
|
+
"traffic light",
|
|
34259
|
+
"fire hydrant",
|
|
34260
|
+
"street sign",
|
|
34261
|
+
"stop sign",
|
|
34262
|
+
"parking meter",
|
|
34263
|
+
"bench",
|
|
34264
|
+
"bird",
|
|
34265
|
+
"cat",
|
|
34266
|
+
"dog",
|
|
34267
|
+
"horse",
|
|
34268
|
+
"sheep",
|
|
34269
|
+
"cow",
|
|
34270
|
+
"elephant",
|
|
34271
|
+
"bear",
|
|
34272
|
+
"zebra",
|
|
34273
|
+
"giraffe",
|
|
34274
|
+
"hat",
|
|
34275
|
+
"backpack",
|
|
34276
|
+
"umbrella",
|
|
34277
|
+
"shoe",
|
|
34278
|
+
"eye glasses",
|
|
34279
|
+
"handbag",
|
|
34280
|
+
"tie",
|
|
34281
|
+
"suitcase",
|
|
34282
|
+
"frisbee",
|
|
34283
|
+
"skis",
|
|
34284
|
+
"snowboard",
|
|
34285
|
+
"sports ball",
|
|
34286
|
+
"kite",
|
|
34287
|
+
"baseball bat",
|
|
34288
|
+
"baseball glove",
|
|
34289
|
+
"skateboard",
|
|
34290
|
+
"surfboard",
|
|
34291
|
+
"tennis racket",
|
|
34292
|
+
"bottle",
|
|
34293
|
+
"plate",
|
|
34294
|
+
"wine glass",
|
|
34295
|
+
"cup",
|
|
34296
|
+
"fork",
|
|
34297
|
+
"knife",
|
|
34298
|
+
"spoon",
|
|
34299
|
+
"bowl",
|
|
34300
|
+
"banana",
|
|
34301
|
+
"apple",
|
|
34302
|
+
"sandwich",
|
|
34303
|
+
"orange",
|
|
34304
|
+
"broccoli",
|
|
34305
|
+
"carrot",
|
|
34306
|
+
"hot dog",
|
|
34307
|
+
"pizza",
|
|
34308
|
+
"donut",
|
|
34309
|
+
"cake",
|
|
34310
|
+
"chair",
|
|
34311
|
+
"couch",
|
|
34312
|
+
"potted plant",
|
|
34313
|
+
"bed",
|
|
34314
|
+
"mirror",
|
|
34315
|
+
"dining table",
|
|
34316
|
+
"window",
|
|
34317
|
+
"desk",
|
|
34318
|
+
"toilet",
|
|
34319
|
+
"door",
|
|
34320
|
+
"tv",
|
|
34321
|
+
"laptop",
|
|
34322
|
+
"mouse",
|
|
34323
|
+
"remote",
|
|
34324
|
+
"keyboard",
|
|
34325
|
+
"cell phone",
|
|
34326
|
+
"microwave",
|
|
34327
|
+
"oven",
|
|
34328
|
+
"toaster",
|
|
34329
|
+
"sink",
|
|
34330
|
+
"refrigerator",
|
|
34331
|
+
"blender",
|
|
34332
|
+
"book",
|
|
34333
|
+
"clock",
|
|
34334
|
+
"vase",
|
|
34335
|
+
"scissors",
|
|
34336
|
+
"teddy bear",
|
|
34337
|
+
"hair drier",
|
|
34338
|
+
"toothbrush",
|
|
34339
|
+
"hair brush"
|
|
34340
|
+
],
|
|
34341
|
+
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."
|
|
34342
|
+
}];
|
|
34343
|
+
/**
|
|
34344
|
+
* Build the registrable descriptor for a curated catalog entry. The URL is
|
|
34345
|
+
* the PUBLIC one — any node downloads it on demand; no Distribute needed.
|
|
34346
|
+
*/
|
|
34347
|
+
function buildFrigateCatalogDescriptor(catalogId, overrides) {
|
|
34348
|
+
const cat = FRIGATE_PUBLIC_CATALOG.find((e) => e.id === catalogId);
|
|
34349
|
+
if (!cat) throw new Error(`Unknown Frigate catalog id "${catalogId}" — available: ${FRIGATE_PUBLIC_CATALOG.map((e) => e.id).join(", ")}`);
|
|
34350
|
+
return {
|
|
34351
|
+
stepId: "object-detection",
|
|
34352
|
+
entry: {
|
|
34353
|
+
id: overrides?.modelId?.trim() || cat.id,
|
|
34354
|
+
name: overrides?.name?.trim() || cat.label,
|
|
34355
|
+
description: `${cat.notes} Imported from the Frigate public catalog via Model Studio.`,
|
|
34356
|
+
formats: { [cat.format]: {
|
|
34357
|
+
url: cat.url,
|
|
34358
|
+
sizeMB: cat.sizeMB,
|
|
34359
|
+
runtimes: ["python"]
|
|
34360
|
+
} },
|
|
34361
|
+
inputSize: {
|
|
34362
|
+
width: cat.inputSize,
|
|
34363
|
+
height: cat.inputSize
|
|
34364
|
+
},
|
|
34365
|
+
labels: cat.labels.map((name) => ({
|
|
34366
|
+
id: name,
|
|
34367
|
+
name
|
|
34368
|
+
})),
|
|
34369
|
+
preprocessMode: "letterbox",
|
|
34370
|
+
...cat.postprocessor !== void 0 ? { postprocessor: cat.postprocessor } : {}
|
|
34371
|
+
}
|
|
34372
|
+
};
|
|
34373
|
+
}
|
|
34374
|
+
//#endregion
|
|
34375
|
+
//#region src/frigate/plus-conversion.ts
|
|
34376
|
+
/** Converted formats ADD to the imported entry; identity fields (decode,
|
|
34377
|
+
* labels, description, preprocess) stay the import's. */
|
|
34378
|
+
function mergeConvertedFormats(base, converted) {
|
|
34379
|
+
return {
|
|
34380
|
+
...base,
|
|
34381
|
+
formats: {
|
|
34382
|
+
...base.formats,
|
|
34383
|
+
...converted.formats
|
|
34384
|
+
}
|
|
34385
|
+
};
|
|
34386
|
+
}
|
|
34387
|
+
function targetOf(format) {
|
|
34388
|
+
return format === "openvino" ? {
|
|
34389
|
+
format,
|
|
34390
|
+
precisions: ["fp16"]
|
|
34391
|
+
} : { format };
|
|
34392
|
+
}
|
|
34393
|
+
/**
|
|
34394
|
+
* Convert the imported ONNX for each requested engine, one target at a time,
|
|
34395
|
+
* merging every success into the registered entry. Returns one outcome per
|
|
34396
|
+
* requested format — the caller surfaces them verbatim.
|
|
34397
|
+
*/
|
|
34398
|
+
async function runPlusConversions(deps, args) {
|
|
34399
|
+
const outcomes = [];
|
|
34400
|
+
let current = args.entry;
|
|
34401
|
+
for (const format of args.targetFormats) {
|
|
34402
|
+
const targets = [targetOf(format)];
|
|
34403
|
+
try {
|
|
34404
|
+
const ranked = await deps.recommend({ targets });
|
|
34405
|
+
const best = ranked.find((r) => r.supported);
|
|
34406
|
+
if (!best) {
|
|
34407
|
+
const reason = ranked[0]?.reason ?? "no node can satisfy this target";
|
|
34408
|
+
outcomes.push({
|
|
34409
|
+
format,
|
|
34410
|
+
ok: false,
|
|
34411
|
+
error: reason
|
|
34412
|
+
});
|
|
34413
|
+
continue;
|
|
34414
|
+
}
|
|
34415
|
+
const sourceUrl = await deps.freshSourceUrl();
|
|
34416
|
+
const result = await deps.convert({
|
|
34417
|
+
sourceUrl,
|
|
34418
|
+
metadata: {
|
|
34419
|
+
id: current.id,
|
|
34420
|
+
name: current.name,
|
|
34421
|
+
labels: current.labels,
|
|
34422
|
+
inputSize: current.inputSize,
|
|
34423
|
+
preprocessMode: current.preprocessMode ?? "letterbox",
|
|
34424
|
+
outputFormat: "yolo"
|
|
34425
|
+
},
|
|
34426
|
+
targets
|
|
34427
|
+
}, best.nodeId);
|
|
34428
|
+
if (result.artifacts.length === 0) {
|
|
34429
|
+
outcomes.push({
|
|
34430
|
+
format,
|
|
34431
|
+
ok: false,
|
|
34432
|
+
nodeId: best.nodeId,
|
|
34433
|
+
error: "no validated artifacts"
|
|
34434
|
+
});
|
|
34435
|
+
continue;
|
|
34436
|
+
}
|
|
34437
|
+
current = mergeConvertedFormats(current, result.entry);
|
|
34438
|
+
await deps.upsertEntry(current);
|
|
34439
|
+
await deps.markAvailable(best.nodeId, current.id, result.artifacts);
|
|
34440
|
+
outcomes.push({
|
|
34441
|
+
format,
|
|
34442
|
+
ok: true,
|
|
34443
|
+
nodeId: best.nodeId
|
|
34444
|
+
});
|
|
34445
|
+
} catch (err) {
|
|
34446
|
+
outcomes.push({
|
|
34447
|
+
format,
|
|
34448
|
+
ok: false,
|
|
34449
|
+
error: err instanceof Error ? err.message : "conversion failed"
|
|
34450
|
+
});
|
|
34451
|
+
}
|
|
34452
|
+
}
|
|
34453
|
+
return outcomes;
|
|
34454
|
+
}
|
|
34455
|
+
//#endregion
|
|
34456
|
+
//#region src/frigate/frigate-plus.ts
|
|
34457
|
+
var PLUS_API_HOST = "https://api.frigate.video";
|
|
34458
|
+
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}$/;
|
|
34459
|
+
function isValidPlusApiKey(key) {
|
|
34460
|
+
return PLUS_KEY_RE.test(key);
|
|
34461
|
+
}
|
|
34462
|
+
function isRecord(value) {
|
|
34463
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
34464
|
+
}
|
|
34465
|
+
/** Exchange the API key for an access token. The key never appears in errors. */
|
|
34466
|
+
async function fetchPlusAccessToken(fetchFn, apiKey) {
|
|
34467
|
+
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");
|
|
34468
|
+
const [user, pass] = apiKey.split(":");
|
|
34469
|
+
const basic = Buffer.from(`${user}:${pass}`).toString("base64");
|
|
34470
|
+
const res = await fetchFn(`${PLUS_API_HOST}/v1/auth/token`, { headers: { authorization: `Basic ${basic}` } });
|
|
34471
|
+
if (!res.ok) throw new Error(`Frigate+ authentication failed (HTTP ${res.status}) — check the API key in the Model Studio addon settings`);
|
|
34472
|
+
const body = await res.json();
|
|
34473
|
+
const token = isRecord(body) && typeof body.accessToken === "string" ? body.accessToken : null;
|
|
34474
|
+
if (token === null) throw new Error("Frigate+ auth response carried no accessToken");
|
|
34475
|
+
return token;
|
|
34476
|
+
}
|
|
34477
|
+
async function plusGet(fetchFn, token, path) {
|
|
34478
|
+
const res = await fetchFn(`${PLUS_API_HOST}/v1/${path}`, { headers: { authorization: `Bearer ${token}` } });
|
|
34479
|
+
if (!res.ok) throw new Error(`Frigate+ request "${path}" failed (HTTP ${res.status}): ${await res.text()}`);
|
|
34480
|
+
return res.json();
|
|
34481
|
+
}
|
|
34482
|
+
/**
|
|
34483
|
+
* `verifiedCounts` has THREE real shapes and none may break the parse:
|
|
34484
|
+
* -1 → base model ("not applicable") ⇒ 0
|
|
34485
|
+
* { "11": 199 } → legacy custom (flat label→count)
|
|
34486
|
+
* { camera: { label: n, … }, … } → recent custom (per camera)
|
|
34487
|
+
*/
|
|
34488
|
+
function verifiedImageCount(raw) {
|
|
34489
|
+
if (typeof raw === "number") return raw > 0 ? raw : 0;
|
|
34490
|
+
if (!isRecord(raw)) return 0;
|
|
34491
|
+
let total = 0;
|
|
34492
|
+
for (const value of Object.values(raw)) if (typeof value === "number") total += value > 0 ? value : 0;
|
|
34493
|
+
else if (isRecord(value)) {
|
|
34494
|
+
for (const inner of Object.values(value)) if (typeof inner === "number" && inner > 0) total += inner;
|
|
34495
|
+
}
|
|
34496
|
+
return total;
|
|
34497
|
+
}
|
|
34498
|
+
function toModelSummary(m) {
|
|
34499
|
+
if (typeof m.id !== "string") return null;
|
|
34500
|
+
return {
|
|
34501
|
+
id: m.id,
|
|
34502
|
+
name: typeof m.name === "string" ? m.name : m.id,
|
|
34503
|
+
type: typeof m.type === "string" ? m.type : "unknown",
|
|
34504
|
+
isBaseModel: m.isBaseModel === true,
|
|
34505
|
+
baseModel: typeof m.baseModel === "string" ? m.baseModel : "",
|
|
34506
|
+
supportedDetectors: Array.isArray(m.supportedDetectors) ? m.supportedDetectors.filter((d) => typeof d === "string") : [],
|
|
34507
|
+
trainDate: typeof m.trainDate === "string" ? m.trainDate : "",
|
|
34508
|
+
...typeof m.hailoDevice === "string" ? { hailoDevice: m.hailoDevice } : {},
|
|
34509
|
+
verifiedImages: verifiedImageCount(m.verifiedCounts),
|
|
34510
|
+
...typeof m.width === "number" ? { width: m.width } : {},
|
|
34511
|
+
...typeof m.height === "number" ? { height: m.height } : {}
|
|
34512
|
+
};
|
|
34513
|
+
}
|
|
34514
|
+
/** Parse a raw `/v1/model/list` body (`{list:[…]}`, or a bare array). */
|
|
34515
|
+
function toPlusModelSummaries(raw) {
|
|
34516
|
+
return (Array.isArray(raw) ? raw : isRecord(raw) && Array.isArray(raw.list) ? raw.list : []).filter(isRecord).map(toModelSummary).filter((m) => m !== null);
|
|
34517
|
+
}
|
|
34518
|
+
/**
|
|
34519
|
+
* EVERYTHING the account has access to — base AND custom models. The real
|
|
34520
|
+
* response wraps the array: `{ "list": [...] }` (a bare array is tolerated
|
|
34521
|
+
* for forward compatibility).
|
|
34522
|
+
*/
|
|
34523
|
+
async function listPlusModels(fetchFn, apiKey) {
|
|
34524
|
+
return toPlusModelSummaries(await plusGet(fetchFn, await fetchPlusAccessToken(fetchFn, apiKey), "model/list"));
|
|
34525
|
+
}
|
|
34526
|
+
function toModelInfo(raw, modelId) {
|
|
34527
|
+
if (!isRecord(raw)) throw new Error("Frigate+ model info: unexpected response shape");
|
|
34528
|
+
const labelMap = {};
|
|
34529
|
+
if (isRecord(raw.labelMap)) {
|
|
34530
|
+
for (const [k, v] of Object.entries(raw.labelMap)) if (typeof v === "string") labelMap[k] = v;
|
|
34531
|
+
}
|
|
34532
|
+
return {
|
|
34533
|
+
id: typeof raw.id === "string" ? raw.id : modelId,
|
|
34534
|
+
type: typeof raw.type === "string" ? raw.type : "unknown",
|
|
34535
|
+
width: typeof raw.width === "number" ? raw.width : 320,
|
|
34536
|
+
height: typeof raw.height === "number" ? raw.height : 320,
|
|
34537
|
+
labelMap
|
|
34538
|
+
};
|
|
34539
|
+
}
|
|
34540
|
+
/** Resolve one model's metadata + presigned artifact URL. */
|
|
34541
|
+
async function requestPlusModelDownload(fetchFn, apiKey, plusModelId) {
|
|
34542
|
+
const token = await fetchPlusAccessToken(fetchFn, apiKey);
|
|
34543
|
+
const info = toModelInfo(await plusGet(fetchFn, token, `model/${plusModelId}`), plusModelId);
|
|
34544
|
+
const signed = await plusGet(fetchFn, token, `model/${plusModelId}/signed_url`);
|
|
34545
|
+
const url = isRecord(signed) && typeof signed.url === "string" ? signed.url : null;
|
|
34546
|
+
if (url === null) throw new Error("Frigate+ signed_url response carried no url");
|
|
34547
|
+
return {
|
|
34548
|
+
info,
|
|
34549
|
+
downloadUrl: url
|
|
34550
|
+
};
|
|
34551
|
+
}
|
|
34552
|
+
/** Frigate+ model type → our format + decode. Mirrors the Frigate presets. */
|
|
34553
|
+
var PLUS_TYPE_MAP = {
|
|
34554
|
+
mobiledet: {
|
|
34555
|
+
format: "tflite",
|
|
34556
|
+
postprocessor: "ssd",
|
|
34557
|
+
extension: ".tflite"
|
|
34558
|
+
},
|
|
34559
|
+
yolonas: {
|
|
34560
|
+
format: "onnx",
|
|
34561
|
+
postprocessor: "yolonas",
|
|
34562
|
+
extension: ".onnx"
|
|
34563
|
+
},
|
|
34564
|
+
yolov9: {
|
|
34565
|
+
format: "onnx",
|
|
34566
|
+
extension: ".onnx"
|
|
34567
|
+
}
|
|
34568
|
+
};
|
|
34569
|
+
function plusTypeTraits(type) {
|
|
34570
|
+
return PLUS_TYPE_MAP[type] ?? {
|
|
34571
|
+
format: "onnx",
|
|
34572
|
+
extension: ".onnx"
|
|
34573
|
+
};
|
|
34574
|
+
}
|
|
34575
|
+
/** Ordered labels from the API's `index → label` map; holes keep alignment. */
|
|
34576
|
+
function orderedLabels(labelMap) {
|
|
34577
|
+
const indices = Object.keys(labelMap).map((k) => Number.parseInt(k, 10)).filter((n) => Number.isInteger(n) && n >= 0);
|
|
34578
|
+
if (indices.length === 0) return [];
|
|
34579
|
+
const max = Math.max(...indices);
|
|
34580
|
+
const out = [];
|
|
34581
|
+
for (let i = 0; i <= max; i++) out.push(labelMap[String(i)] ?? `unknown-${i}`);
|
|
34582
|
+
return out;
|
|
34583
|
+
}
|
|
34584
|
+
/**
|
|
34585
|
+
* Build the registrable descriptor for a downloaded Frigate+ model. The
|
|
34586
|
+
* artifact is hub-local (landed by the server-side download), so the URL uses
|
|
34587
|
+
* the `camstack-local://` convention (D208) — other nodes get it via
|
|
34588
|
+
* Distribute.
|
|
34589
|
+
*/
|
|
34590
|
+
function plusInfoToDescriptor(info, opts) {
|
|
34591
|
+
const traits = plusTypeTraits(info.type);
|
|
34592
|
+
const labels = orderedLabels(info.labelMap);
|
|
34593
|
+
return {
|
|
34594
|
+
stepId: "object-detection",
|
|
34595
|
+
entry: {
|
|
34596
|
+
id: opts.modelId,
|
|
34597
|
+
name: opts.name,
|
|
34598
|
+
description: `Frigate+ personal model (${info.type}) — imported via Model Studio`,
|
|
34599
|
+
formats: { [traits.format]: {
|
|
34600
|
+
url: `camstack-local://${opts.modelId}/${opts.filename}`,
|
|
34601
|
+
sizeMB: opts.sizeMB,
|
|
34602
|
+
runtimes: ["python"]
|
|
34603
|
+
} },
|
|
34604
|
+
inputSize: {
|
|
34605
|
+
width: info.width,
|
|
34606
|
+
height: info.height
|
|
34607
|
+
},
|
|
34608
|
+
labels: labels.map((name) => ({
|
|
34609
|
+
id: name,
|
|
34610
|
+
name
|
|
34611
|
+
})),
|
|
34612
|
+
preprocessMode: "letterbox",
|
|
34613
|
+
...traits.postprocessor !== void 0 ? { postprocessor: traits.postprocessor } : {}
|
|
34614
|
+
}
|
|
34615
|
+
};
|
|
34616
|
+
}
|
|
34170
34617
|
var MODEL_UPLOAD_ALLOWED_EXTENSIONS = [".onnx", ".tflite"];
|
|
34171
34618
|
var FILENAME_RE = /^[a-zA-Z0-9._-]+$/;
|
|
34172
34619
|
/**
|
|
@@ -34308,6 +34755,78 @@ var modelStudioActions = defineCustomActions({
|
|
|
34308
34755
|
}), { kind: "mutation" }),
|
|
34309
34756
|
abortModelUpload: customAction(object({ uploadId: string() }), object({ ok: literal(true) }), { kind: "mutation" }),
|
|
34310
34757
|
/**
|
|
34758
|
+
* Curated PUBLIC Frigate model catalog (D214 addendum) — models Frigate
|
|
34759
|
+
* uses/references that are downloadable without credentials, with verified
|
|
34760
|
+
* metadata so the operator types nothing. Import downloads SERVER-SIDE into
|
|
34761
|
+
* the hub's modelsDir and registers the descriptor with the public URL
|
|
34762
|
+
* (other nodes download on demand — no Distribute needed).
|
|
34763
|
+
*/
|
|
34764
|
+
listFrigateCatalog: customAction(_void(), array(object({
|
|
34765
|
+
id: string(),
|
|
34766
|
+
label: string(),
|
|
34767
|
+
format: _enum(MODEL_FORMATS),
|
|
34768
|
+
sizeMB: number(),
|
|
34769
|
+
inputSize: number(),
|
|
34770
|
+
notes: string()
|
|
34771
|
+
})).readonly()),
|
|
34772
|
+
importFrigateCatalogModel: customAction(object({
|
|
34773
|
+
catalogId: string(),
|
|
34774
|
+
modelId: string().optional(),
|
|
34775
|
+
name: string().optional()
|
|
34776
|
+
}), object({
|
|
34777
|
+
ok: literal(true),
|
|
34778
|
+
modelId: string(),
|
|
34779
|
+
bytes: number()
|
|
34780
|
+
}), { kind: "mutation" }),
|
|
34781
|
+
/**
|
|
34782
|
+
* Frigate+ personal models. The API key lives in the addon's SETTINGS
|
|
34783
|
+
* (password field) and never leaves the hub: list / test / import all run
|
|
34784
|
+
* server-side against api.frigate.video. Status deliberately reveals only
|
|
34785
|
+
* booleans — never the key.
|
|
34786
|
+
*/
|
|
34787
|
+
/**
|
|
34788
|
+
* Nodes whose engine can actually LOAD a tflite artifact (the pool's only
|
|
34789
|
+
* tflite runtime binds the Coral EdgeTPU delegate — no Coral, no tflite).
|
|
34790
|
+
* The UI gates tflite imports on this instead of letting them fail at load.
|
|
34791
|
+
*/
|
|
34792
|
+
tfliteSupport: customAction(_void(), object({ nodes: array(string()).readonly() })),
|
|
34793
|
+
frigatePlusStatus: customAction(_void(), object({
|
|
34794
|
+
configured: boolean(),
|
|
34795
|
+
keyFormatValid: boolean()
|
|
34796
|
+
})),
|
|
34797
|
+
frigatePlusTest: customAction(_void(), object({
|
|
34798
|
+
ok: boolean(),
|
|
34799
|
+
error: string().optional()
|
|
34800
|
+
}), { kind: "mutation" }),
|
|
34801
|
+
frigatePlusListModels: customAction(_void(), array(object({
|
|
34802
|
+
id: string(),
|
|
34803
|
+
name: string(),
|
|
34804
|
+
type: string(),
|
|
34805
|
+
isBaseModel: boolean(),
|
|
34806
|
+
baseModel: string(),
|
|
34807
|
+
supportedDetectors: array(string()).readonly(),
|
|
34808
|
+
trainDate: string(),
|
|
34809
|
+
hailoDevice: string().optional(),
|
|
34810
|
+
verifiedImages: number(),
|
|
34811
|
+
width: number().optional(),
|
|
34812
|
+
height: number().optional()
|
|
34813
|
+
})).readonly()),
|
|
34814
|
+
importFrigatePlusModel: customAction(object({
|
|
34815
|
+
plusModelId: string(),
|
|
34816
|
+
modelId: string(),
|
|
34817
|
+
name: string().optional()
|
|
34818
|
+
}), object({
|
|
34819
|
+
ok: literal(true),
|
|
34820
|
+
modelId: string(),
|
|
34821
|
+
bytes: number(),
|
|
34822
|
+
conversions: array(object({
|
|
34823
|
+
format: _enum(["openvino", "coreml"]),
|
|
34824
|
+
ok: boolean(),
|
|
34825
|
+
nodeId: string().optional(),
|
|
34826
|
+
error: string().optional()
|
|
34827
|
+
})).readonly()
|
|
34828
|
+
}), { kind: "mutation" }),
|
|
34829
|
+
/**
|
|
34311
34830
|
* Rank nodes for model conversion based on hardware capabilities.
|
|
34312
34831
|
* CoreML targets require darwin; OpenVINO targets require an OpenVINO-capable node.
|
|
34313
34832
|
*/
|
|
@@ -34390,18 +34909,9 @@ function spawnRunConvert(jobPath, pythonPath, onProgress, _sessionId) {
|
|
|
34390
34909
|
});
|
|
34391
34910
|
});
|
|
34392
34911
|
}
|
|
34393
|
-
/**
|
|
34394
|
-
|
|
34395
|
-
|
|
34396
|
-
*
|
|
34397
|
-
* P1 maintained the durable list of custom detection models and exposed them
|
|
34398
|
-
* so the detection pipeline can offer them in its model picker.
|
|
34399
|
-
*
|
|
34400
|
-
* P3 adds the `model-convert` provider on EVERY node so agents can run
|
|
34401
|
-
* local Python conversion without routing through the hub.
|
|
34402
|
-
* The `custom-model-registry` cap and all customActions are hub-only.
|
|
34403
|
-
*/
|
|
34404
|
-
var ModelStudioAddon = class extends BaseAddon {
|
|
34912
|
+
/** Global-fetch adapter for the injected Frigate+ client seam. */
|
|
34913
|
+
var plusFetch = async (url, init) => fetch(url, init);
|
|
34914
|
+
var ModelStudioAddon = class ModelStudioAddon extends BaseAddon {
|
|
34405
34915
|
id = "model-studio";
|
|
34406
34916
|
modelsState = null;
|
|
34407
34917
|
availabilityState = null;
|
|
@@ -34423,7 +34933,21 @@ var ModelStudioAddon = class extends BaseAddon {
|
|
|
34423
34933
|
section: "detection"
|
|
34424
34934
|
}];
|
|
34425
34935
|
constructor() {
|
|
34426
|
-
super({});
|
|
34936
|
+
super({ frigatePlusApiKey: "" });
|
|
34937
|
+
}
|
|
34938
|
+
globalSettingsSchema() {
|
|
34939
|
+
return this.schema({ sections: [{
|
|
34940
|
+
id: "frigate-plus",
|
|
34941
|
+
title: "Frigate+",
|
|
34942
|
+
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.",
|
|
34943
|
+
fields: [{
|
|
34944
|
+
type: "password",
|
|
34945
|
+
key: "frigatePlusApiKey",
|
|
34946
|
+
label: "Frigate+ API key",
|
|
34947
|
+
description: "Shape: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:40-hex-chars",
|
|
34948
|
+
showToggle: true
|
|
34949
|
+
}]
|
|
34950
|
+
}] });
|
|
34427
34951
|
}
|
|
34428
34952
|
makeConvertDeps() {
|
|
34429
34953
|
return {
|
|
@@ -34510,6 +35034,45 @@ var ModelStudioAddon = class extends BaseAddon {
|
|
|
34510
35034
|
listCustomModels: async () => this.listCustomModels(),
|
|
34511
35035
|
distributeModel: async (input) => this.distributeModel(input),
|
|
34512
35036
|
listAvailability: async () => this.availability_.get(),
|
|
35037
|
+
listFrigateCatalog: async () => FRIGATE_PUBLIC_CATALOG.map((c) => ({
|
|
35038
|
+
id: c.id,
|
|
35039
|
+
label: c.label,
|
|
35040
|
+
format: c.format,
|
|
35041
|
+
sizeMB: c.sizeMB,
|
|
35042
|
+
inputSize: c.inputSize,
|
|
35043
|
+
notes: c.notes
|
|
35044
|
+
})),
|
|
35045
|
+
importFrigateCatalogModel: async (input) => this.importFrigateCatalogModel(input),
|
|
35046
|
+
tfliteSupport: async () => {
|
|
35047
|
+
const api = this.ctx.api;
|
|
35048
|
+
if (!api) return { nodes: [] };
|
|
35049
|
+
const nodes = await this.recommendDeps().listNodes();
|
|
35050
|
+
const supported = [];
|
|
35051
|
+
for (const nId of nodes) try {
|
|
35052
|
+
if ((await api.platformProbe.getCapabilities.query({ nodeId: nId })).scores.some((sc) => sc.format === "tflite" && sc.available)) supported.push(nId);
|
|
35053
|
+
} catch {}
|
|
35054
|
+
return { nodes: supported };
|
|
35055
|
+
},
|
|
35056
|
+
frigatePlusStatus: async () => {
|
|
35057
|
+
const key = this.config.frigatePlusApiKey ?? "";
|
|
35058
|
+
return {
|
|
35059
|
+
configured: key.length > 0,
|
|
35060
|
+
keyFormatValid: isValidPlusApiKey(key)
|
|
35061
|
+
};
|
|
35062
|
+
},
|
|
35063
|
+
frigatePlusTest: async () => {
|
|
35064
|
+
try {
|
|
35065
|
+
await fetchPlusAccessToken(plusFetch, this.config.frigatePlusApiKey ?? "");
|
|
35066
|
+
return { ok: true };
|
|
35067
|
+
} catch (err) {
|
|
35068
|
+
return {
|
|
35069
|
+
ok: false,
|
|
35070
|
+
error: err instanceof Error ? err.message : "auth failed"
|
|
35071
|
+
};
|
|
35072
|
+
}
|
|
35073
|
+
},
|
|
35074
|
+
frigatePlusListModels: async () => listPlusModels(plusFetch, this.config.frigatePlusApiKey ?? ""),
|
|
35075
|
+
importFrigatePlusModel: async (input) => this.importFrigatePlusModel(input),
|
|
34513
35076
|
recommendConvertNode: async (input) => {
|
|
34514
35077
|
const api = this.ctx.api;
|
|
34515
35078
|
if (!api) throw new Error("recommendConvertNode: ctx.api unavailable");
|
|
@@ -34639,6 +35202,170 @@ var ModelStudioAddon = class extends BaseAddon {
|
|
|
34639
35202
|
bytes: res.bytes
|
|
34640
35203
|
};
|
|
34641
35204
|
}
|
|
35205
|
+
/** listNodes/hardwareOf deps for `recommendConvertNode` — shared by the
|
|
35206
|
+
* convert actions and the Frigate+ auto-conversion. */
|
|
35207
|
+
recommendDeps() {
|
|
35208
|
+
const api = this.ctx.api;
|
|
35209
|
+
if (!api) throw new Error("model-studio: ctx.api unavailable");
|
|
35210
|
+
return {
|
|
35211
|
+
listNodes: async () => {
|
|
35212
|
+
return (await api.addons.listCapabilityProviders.query({ capName: "model-convert" })).map((p) => {
|
|
35213
|
+
const atIdx = p.addonId.indexOf("@");
|
|
35214
|
+
return atIdx !== -1 ? p.addonId.slice(atIdx + 1) : "hub";
|
|
35215
|
+
});
|
|
35216
|
+
},
|
|
35217
|
+
hardwareOf: async (nId) => {
|
|
35218
|
+
const hw = await api.platformProbe.getHardware.query({ nodeId: nId });
|
|
35219
|
+
const caps = await api.platformProbe.getCapabilities.query({ nodeId: nId });
|
|
35220
|
+
return {
|
|
35221
|
+
platform: hw.platform,
|
|
35222
|
+
gpu: hw.gpu ?? void 0,
|
|
35223
|
+
npu: hw.npu ?? void 0,
|
|
35224
|
+
openvino: caps.scores.some((s) => s.format === "openvino" && s.available)
|
|
35225
|
+
};
|
|
35226
|
+
}
|
|
35227
|
+
};
|
|
35228
|
+
}
|
|
35229
|
+
/** sha256 of a file on disk (streamed). */
|
|
35230
|
+
static async fileSha256(filePath) {
|
|
35231
|
+
const hash = node_crypto.default.createHash("sha256");
|
|
35232
|
+
await new Promise((resolve, reject) => {
|
|
35233
|
+
node_fs.default.createReadStream(filePath).on("data", (chunk) => hash.update(chunk)).on("end", () => resolve()).on("error", reject);
|
|
35234
|
+
});
|
|
35235
|
+
return hash.digest("hex");
|
|
35236
|
+
}
|
|
35237
|
+
/**
|
|
35238
|
+
* Import a curated PUBLIC Frigate catalog model: download SERVER-SIDE into
|
|
35239
|
+
* the hub's modelsDir (size-verified against the curated declaration),
|
|
35240
|
+
* register the descriptor with the PUBLIC url (nodes download on demand),
|
|
35241
|
+
* and record hub availability.
|
|
35242
|
+
*/
|
|
35243
|
+
async importFrigateCatalogModel(input) {
|
|
35244
|
+
const cat = FRIGATE_PUBLIC_CATALOG.find((c) => c.id === input.catalogId);
|
|
35245
|
+
if (!cat) throw new Error(`importFrigateCatalogModel: unknown catalog id "${input.catalogId}"`);
|
|
35246
|
+
const descriptor = buildFrigateCatalogDescriptor(input.catalogId, {
|
|
35247
|
+
...input.modelId !== void 0 ? { modelId: input.modelId } : {},
|
|
35248
|
+
...input.name !== void 0 ? { name: input.name } : {}
|
|
35249
|
+
});
|
|
35250
|
+
const modelsDir = await this.resolveModelsDir();
|
|
35251
|
+
const filename = cat.url.split("/").at(-1) ?? `${descriptor.entry.id}.${cat.format}`;
|
|
35252
|
+
const destPath = node_path.default.join(modelsDir, filename);
|
|
35253
|
+
await downloadFile(cat.url, destPath);
|
|
35254
|
+
const bytes = (await node_fs.default.promises.stat(destPath)).size;
|
|
35255
|
+
const declaredBytes = cat.sizeMB * 1e6;
|
|
35256
|
+
if (bytes < declaredBytes * .5 || bytes > declaredBytes * 2) {
|
|
35257
|
+
await node_fs.default.promises.rm(destPath, { force: true });
|
|
35258
|
+
throw new Error(`importFrigateCatalogModel: downloaded ${bytes} bytes but the catalog declares ~${Math.round(declaredBytes)} — refusing to register a suspicious artifact`);
|
|
35259
|
+
}
|
|
35260
|
+
const sha256 = await ModelStudioAddon.fileSha256(destPath);
|
|
35261
|
+
await this.registerCustomModel(descriptor);
|
|
35262
|
+
await this.availability_.update((prev) => upsertAvailability(prev, descriptor.entry.id, "hub", {
|
|
35263
|
+
format: cat.format,
|
|
35264
|
+
sha256,
|
|
35265
|
+
bytes,
|
|
35266
|
+
at: Date.now()
|
|
35267
|
+
}));
|
|
35268
|
+
this.ctx.logger.info("Imported Frigate catalog model", { meta: {
|
|
35269
|
+
catalogId: input.catalogId,
|
|
35270
|
+
modelId: descriptor.entry.id,
|
|
35271
|
+
bytes,
|
|
35272
|
+
sha256
|
|
35273
|
+
} });
|
|
35274
|
+
return {
|
|
35275
|
+
ok: true,
|
|
35276
|
+
modelId: descriptor.entry.id,
|
|
35277
|
+
bytes
|
|
35278
|
+
};
|
|
35279
|
+
}
|
|
35280
|
+
/**
|
|
35281
|
+
* Import one of the operator's personal Frigate+ models: resolve metadata +
|
|
35282
|
+
* presigned URL with the SECRET key (server-side only), download into the
|
|
35283
|
+
* hub's modelsDir, and register with the model-type-derived characteristics
|
|
35284
|
+
* (format, decode, input size, labelmap — nothing hand-typed). The artifact
|
|
35285
|
+
* is hub-local: other nodes get it via Models → Distribute.
|
|
35286
|
+
*/
|
|
35287
|
+
async importFrigatePlusModel(input) {
|
|
35288
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(input.modelId)) throw new Error(`importFrigatePlusModel: model id "${input.modelId}" is invalid`);
|
|
35289
|
+
const key = this.config.frigatePlusApiKey ?? "";
|
|
35290
|
+
const { info, downloadUrl } = await requestPlusModelDownload(plusFetch, key, input.plusModelId);
|
|
35291
|
+
const traits = plusTypeTraits(info.type);
|
|
35292
|
+
const filename = `${input.modelId}${traits.extension}`;
|
|
35293
|
+
const modelsDir = await this.resolveModelsDir();
|
|
35294
|
+
const destPath = node_path.default.join(modelsDir, filename);
|
|
35295
|
+
await downloadFile(downloadUrl, destPath);
|
|
35296
|
+
const bytes = (await node_fs.default.promises.stat(destPath)).size;
|
|
35297
|
+
if (bytes < 1e4) {
|
|
35298
|
+
await node_fs.default.promises.rm(destPath, { force: true });
|
|
35299
|
+
throw new Error(`importFrigatePlusModel: downloaded only ${bytes} bytes — the signed URL likely expired or the response was an error page`);
|
|
35300
|
+
}
|
|
35301
|
+
const sha256 = await ModelStudioAddon.fileSha256(destPath);
|
|
35302
|
+
const descriptor = plusInfoToDescriptor(info, {
|
|
35303
|
+
modelId: input.modelId,
|
|
35304
|
+
name: input.name?.trim() || `Frigate+ ${info.type} (${input.plusModelId.slice(0, 8)})`,
|
|
35305
|
+
filename,
|
|
35306
|
+
sizeMB: Number((bytes / 1e6).toFixed(1))
|
|
35307
|
+
});
|
|
35308
|
+
await this.registerCustomModel(descriptor);
|
|
35309
|
+
await this.availability_.update((prev) => upsertAvailability(prev, input.modelId, "hub", {
|
|
35310
|
+
format: traits.format,
|
|
35311
|
+
sha256,
|
|
35312
|
+
bytes,
|
|
35313
|
+
at: Date.now()
|
|
35314
|
+
}));
|
|
35315
|
+
this.ctx.logger.info("Imported Frigate+ model", { meta: {
|
|
35316
|
+
plusModelId: input.plusModelId,
|
|
35317
|
+
modelId: input.modelId,
|
|
35318
|
+
type: info.type,
|
|
35319
|
+
bytes
|
|
35320
|
+
} });
|
|
35321
|
+
let conversions = [];
|
|
35322
|
+
if (traits.format === "onnx") {
|
|
35323
|
+
const api = this.ctx.api;
|
|
35324
|
+
if (api) {
|
|
35325
|
+
conversions = await runPlusConversions({
|
|
35326
|
+
recommend: (i) => recommendConvertNode(i, this.recommendDeps()),
|
|
35327
|
+
convert: (convertInput, nId) => api.modelConvert.convert.mutate(convertInput, nodePin(nId)),
|
|
35328
|
+
freshSourceUrl: async () => (await requestPlusModelDownload(plusFetch, key, input.plusModelId)).downloadUrl,
|
|
35329
|
+
upsertEntry: async (entry) => {
|
|
35330
|
+
await this.state_.update((prev) => upsertCustomModel(prev, {
|
|
35331
|
+
stepId: descriptor.stepId,
|
|
35332
|
+
entry
|
|
35333
|
+
}));
|
|
35334
|
+
},
|
|
35335
|
+
markAvailable: async (nId, mId, artifacts) => {
|
|
35336
|
+
await this.availability_.update((prev) => {
|
|
35337
|
+
const seen = /* @__PURE__ */ new Set();
|
|
35338
|
+
let next = prev;
|
|
35339
|
+
for (const artifact of artifacts) {
|
|
35340
|
+
if (seen.has(artifact.format)) continue;
|
|
35341
|
+
seen.add(artifact.format);
|
|
35342
|
+
next = upsertAvailability(next, mId, nId, {
|
|
35343
|
+
format: artifact.format,
|
|
35344
|
+
sha256: "",
|
|
35345
|
+
bytes: Math.round(artifact.sizeMB * 1e6),
|
|
35346
|
+
at: Date.now()
|
|
35347
|
+
});
|
|
35348
|
+
}
|
|
35349
|
+
return next;
|
|
35350
|
+
});
|
|
35351
|
+
}
|
|
35352
|
+
}, {
|
|
35353
|
+
entry: descriptor.entry,
|
|
35354
|
+
targetFormats: ["openvino", "coreml"]
|
|
35355
|
+
});
|
|
35356
|
+
this.ctx.logger.info("Frigate+ import — engine conversions", { meta: {
|
|
35357
|
+
modelId: input.modelId,
|
|
35358
|
+
outcomes: conversions.map((o) => ({ ...o }))
|
|
35359
|
+
} });
|
|
35360
|
+
}
|
|
35361
|
+
}
|
|
35362
|
+
return {
|
|
35363
|
+
ok: true,
|
|
35364
|
+
modelId: input.modelId,
|
|
35365
|
+
bytes,
|
|
35366
|
+
conversions
|
|
35367
|
+
};
|
|
35368
|
+
}
|
|
34642
35369
|
async registerCustomModel(input) {
|
|
34643
35370
|
if (!input.stepId.trim()) throw new Error("registerCustomModel: stepId is required");
|
|
34644
35371
|
if (!input.entry.id.trim()) throw new Error("registerCustomModel: entry.id is required");
|