@camstack/addon-pipeline 1.1.44 → 1.1.45
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/audio-analyzer/index.js +1 -1
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/detection-pipeline/index.js +2330 -2061
- package/dist/detection-pipeline/index.mjs +2330 -2061
- package/dist/{dist-CySHUkXY.js → dist-CjxqjgEv.js} +39 -3
- package/dist/{dist-CgBGV988.mjs → dist-D04numuO.mjs} +39 -3
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +2 -2
- package/dist/pipeline-runner/index.mjs +2 -2
- package/dist/recorder/index.js +1 -1
- package/dist/recorder/index.mjs +1 -1
- package/dist/{remote-source-plane-D9m0z1N9.js → remote-source-plane-CLuxyDV2.js} +1 -1
- package/dist/{remote-source-plane-B4hRwchu.mjs → remote-source-plane-D31IILFN.mjs} +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-G6wtUb3G.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BGKueo9D.mjs} +2 -2
- package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Dlagq9X_.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D0sKyXiy.mjs} +1 -1
- package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BUpF966l.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-8BUVCFIR.mjs} +1 -1
- package/dist/stream-broker/{hostInit-BSzP3BIj.mjs → hostInit-DazV2RCq.mjs} +2 -2
- package/dist/stream-broker/index.js +2 -2
- package/dist/stream-broker/index.mjs +2 -2
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-D2ML5TPi.js → MaskShapeCanvas-DI4BY7W2-D_F4a_3H.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-TTWGGaqv.js → MotionZonesSettings-NcxxQN8r-CfTBrn4G.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-CJqHU2nU.js → PrivacyMaskSettings-APgPLF7p-BWYkeoRj.js} +1 -1
- package/embed-dist/assets/{index-C1oaL9s7.js → index-_ccEdNW7.js} +5 -5
- package/embed-dist/index.html +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __require } from "../chunk-BdkLduGY.mjs";
|
|
2
|
-
import { H as createEvent, I as errMsg, L as BaseAddon, O as pipelineExecutorCapability, P as supportedRuntimes$1, S as hfModelUrl, U as hydrateSchema, V as EventCategory, X as sleep, a as COCO_TO_MACRO, b as detectionPipelineCapability, f as YAMNET_TO_MACRO, i as COCO_80_LABELS, j as runtimeDevices$1, q as parseJsonUnknown, r as AUDIO_MACRO_LABELS, t as APPLE_SA_TO_MACRO, v as defaultDeviceFor$1, x as evaluateZoneRules } from "../dist-
|
|
2
|
+
import { H as createEvent, I as errMsg, L as BaseAddon, O as pipelineExecutorCapability, P as supportedRuntimes$1, S as hfModelUrl, U as hydrateSchema, V as EventCategory, X as sleep, a as COCO_TO_MACRO, b as detectionPipelineCapability, f as YAMNET_TO_MACRO, i as COCO_80_LABELS, j as runtimeDevices$1, q as parseJsonUnknown, r as AUDIO_MACRO_LABELS, t as APPLE_SA_TO_MACRO, v as defaultDeviceFor$1, x as evaluateZoneRules } from "../dist-D04numuO.mjs";
|
|
3
3
|
import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
|
|
4
4
|
import { a as isModelDownloaded, i as ensureModel, n as deleteModelFromDisk } from "../model-download-service-C-IHWnXx-BPy6aoAx.mjs";
|
|
5
5
|
import { FrameRingReaderCache } from "@camstack/shm-ring";
|
|
@@ -290,2170 +290,2357 @@ function projectNodeEngine(store, nodeId) {
|
|
|
290
290
|
return out;
|
|
291
291
|
}
|
|
292
292
|
//#endregion
|
|
293
|
-
//#region src/detection-pipeline/
|
|
293
|
+
//#region src/detection-pipeline/registry/model-catalogs.ts
|
|
294
|
+
var HF_REPO = "camstack/camstack-models";
|
|
295
|
+
var HF_SCRYPTED = "scrypted/plugin-models";
|
|
296
|
+
var hf = (path) => hfModelUrl(HF_REPO, path);
|
|
297
|
+
var hfScrypted = (path) => hfModelUrl(HF_SCRYPTED, path);
|
|
294
298
|
/**
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
* Multi-process orchestrator. Owns N `PoolWorker` instances, each
|
|
298
|
-
* holding its own Python subprocess + MLModel copy, and round-robins
|
|
299
|
-
* inference dispatches across them. Model management (load/unload/
|
|
300
|
-
* replace) propagates to all workers so every subprocess holds the
|
|
301
|
-
* same model set at the same indices.
|
|
299
|
+
* Build an OpenVINO format entry (always python runtime).
|
|
302
300
|
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
var MSG_INFER_RAW = 2;
|
|
310
|
-
var MSG_CACHE_FRAME = 4;
|
|
311
|
-
var MSG_INFER_CACHED = 5;
|
|
312
|
-
/**
|
|
313
|
-
* Cross-camera batching opcode — N items packed into a single IPC
|
|
314
|
-
* frame. Wire shape:
|
|
315
|
-
* [1B model_idx][1B count][N × (4B w | 4B h | 1B fmt | 4B size | raw bytes)]
|
|
316
|
-
* Python pool dispatches each item via `dispatcher.run` (asyncio.gather)
|
|
317
|
-
* and returns a JSON array of detections in the same order.
|
|
318
|
-
*/
|
|
319
|
-
var MSG_INFER_BATCH = 3;
|
|
320
|
-
var PREFIX_LEN = 9;
|
|
321
|
-
/**
|
|
322
|
-
* Wire-level enum for the raw-frame fast path. Values are append-only:
|
|
323
|
-
* the Python pool reads the byte directly off the IPC frame; reordering
|
|
324
|
-
* existing entries breaks all in-flight workers.
|
|
301
|
+
* OpenVINO IR is a two-file bundle: a `.xml` topology + a sibling `.bin`
|
|
302
|
+
* weights file with the same basename. We declare the `.bin` in `files` so
|
|
303
|
+
* the (format-agnostic) downloader fetches it alongside the `.xml` — without
|
|
304
|
+
* the weights, OpenVINO compile fails with "Empty weights data in bin file".
|
|
305
|
+
* A plain `.onnx` run through the OpenVINO runtime (e.g. yamnet) has no
|
|
306
|
+
* sibling, so none is added.
|
|
325
307
|
*/
|
|
326
|
-
var
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
device = "cpu";
|
|
336
|
-
constructor(pool, modelIndex) {
|
|
337
|
-
this.pool = pool;
|
|
338
|
-
this.modelIndex = modelIndex;
|
|
339
|
-
}
|
|
340
|
-
async infer(input) {
|
|
341
|
-
const start = performance.now();
|
|
342
|
-
return {
|
|
343
|
-
structured: input.kind === "raw" ? await this.pool.inferRaw(this.modelIndex, input.data, input.width, input.height, input.format) : await this.pool.infer(this.modelIndex, input.data),
|
|
344
|
-
inferenceMs: performance.now() - start
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
/**
|
|
348
|
-
* Inference on a frame previously cached in the Python pool via
|
|
349
|
-
* `pool.cacheFrame()`. Sends only 5 bytes through the pipe instead
|
|
350
|
-
* of 1.2MB raw data — eliminates the pipe transfer bottleneck.
|
|
351
|
-
*/
|
|
352
|
-
async inferFromCache(frameId) {
|
|
353
|
-
const start = performance.now();
|
|
354
|
-
return {
|
|
355
|
-
structured: await this.pool.inferCached(this.modelIndex, frameId),
|
|
356
|
-
inferenceMs: performance.now() - start
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
async dispose() {}
|
|
308
|
+
var ovFormat = (url, sizeMB) => {
|
|
309
|
+
const base = url.split("/").pop() ?? "";
|
|
310
|
+
const files = base.endsWith(".xml") ? [base.replace(/\.xml$/, ".bin")] : void 0;
|
|
311
|
+
return {
|
|
312
|
+
url,
|
|
313
|
+
sizeMB,
|
|
314
|
+
runtimes: ["python"],
|
|
315
|
+
...files ? { files } : {}
|
|
316
|
+
};
|
|
360
317
|
};
|
|
361
318
|
/**
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
319
|
+
* Build a precision-variant catalog entry (OpenVINO-only) derived from a base
|
|
320
|
+
* detection model. fp16 halves the weights (Intel iGPU/NPU sweet spot); int8 is
|
|
321
|
+
* NNCF post-training-quantized (~4× smaller, fastest on CPU/iGPU at a small
|
|
322
|
+
* accuracy cost). The IRs live next to the base `.xml` on HF as
|
|
323
|
+
* `camstack-<id>-<precision>.xml`. Lets an operator scale the model to the node
|
|
324
|
+
* (e.g. yolo26x-int8 on a 265K, yolo26n-int8 on an N100).
|
|
365
325
|
*/
|
|
366
|
-
var
|
|
326
|
+
var ovPrecisionVariant = (baseId, ovDir, baseName, precision, sizeMB, legacy = false) => ({
|
|
327
|
+
id: `${baseId}-${precision}`,
|
|
328
|
+
name: `${baseName} (${precision.toUpperCase()})`,
|
|
329
|
+
description: `${baseName} — OpenVINO ${precision.toUpperCase()} variant for Intel iGPU/NPU; scale by hardware`,
|
|
330
|
+
inputSize: {
|
|
331
|
+
width: 640,
|
|
332
|
+
height: 640
|
|
333
|
+
},
|
|
334
|
+
labels: [],
|
|
335
|
+
preprocessMode: "letterbox",
|
|
336
|
+
formats: { openvino: ovFormat(hf(`${ovDir}/camstack-${baseId}-${precision}.xml`), sizeMB) },
|
|
337
|
+
...legacy ? { legacy: true } : {},
|
|
338
|
+
...precision === "int8" ? { group: {
|
|
339
|
+
family: baseId.slice(0, -1),
|
|
340
|
+
tier: baseId.slice(-1),
|
|
341
|
+
precision: "int8"
|
|
342
|
+
} } : {}
|
|
343
|
+
});
|
|
344
|
+
var MLPACKAGE_FILES = [
|
|
345
|
+
"Manifest.json",
|
|
346
|
+
"Data/com.apple.CoreML/model.mlmodel",
|
|
347
|
+
"Data/com.apple.CoreML/weights/weight.bin"
|
|
348
|
+
];
|
|
349
|
+
var YOLO26_TIER_NAME = {
|
|
350
|
+
n: "Nano",
|
|
351
|
+
s: "Small",
|
|
352
|
+
m: "Medium",
|
|
353
|
+
l: "Large"
|
|
354
|
+
};
|
|
367
355
|
/**
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
* escalation guarantees the process dies. Exported for tests.
|
|
356
|
+
* Reduced-input YOLO26 variants (2026-07-11). A lower square input (320 / 256)
|
|
357
|
+
* is the biggest, cheapest latency lever measured across every runtime — Mac
|
|
358
|
+
* ANE, Intel Ultra NPU, and especially the N100 — at a small-object accuracy
|
|
359
|
+
* cost. Each resolution ships a base (fp32) entry (onnx + coreml + openvino)
|
|
360
|
+
* grouped under `{family, tier, resolution}`, plus an OpenVINO INT8 entry (best
|
|
361
|
+
* on Intel/N100) grouped with `precision:'int8'`. The grouped selector surfaces
|
|
362
|
+
* `resolution` as a variant axis alongside precision.
|
|
376
363
|
*/
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
process = null;
|
|
408
|
-
receiveBuffer = Buffer.alloc(0);
|
|
409
|
-
pending = /* @__PURE__ */ new Map();
|
|
410
|
-
nextRequestId = 1;
|
|
411
|
-
ready = false;
|
|
412
|
-
log;
|
|
413
|
-
opts;
|
|
414
|
-
constructor(opts) {
|
|
415
|
-
this.opts = opts;
|
|
416
|
-
this.log = opts.logger;
|
|
417
|
-
}
|
|
418
|
-
getPid() {
|
|
419
|
-
return this.process?.pid ?? null;
|
|
420
|
-
}
|
|
421
|
-
isReady() {
|
|
422
|
-
return this.ready;
|
|
423
|
-
}
|
|
424
|
-
async initialize(initialModels) {
|
|
425
|
-
this.process = spawn(this.opts.pythonPath, [this.opts.scriptPath], { stdio: [
|
|
426
|
-
"pipe",
|
|
427
|
-
"pipe",
|
|
428
|
-
"pipe"
|
|
429
|
-
] });
|
|
430
|
-
if (!this.process.stdout || !this.process.stdin) throw new Error("PoolWorker: failed to create process pipes");
|
|
431
|
-
this.process.stderr?.on("data", (chunk) => {
|
|
432
|
-
for (const line of chunk.toString().split("\n")) {
|
|
433
|
-
const trimmed = line.trim();
|
|
434
|
-
if (trimmed) this.log.warn(`[${this.opts.workerLabel}] ${trimmed}`);
|
|
435
|
-
}
|
|
436
|
-
});
|
|
437
|
-
this.process.on("error", (err) => {
|
|
438
|
-
this.log.error("Worker process error", { meta: {
|
|
439
|
-
worker: this.opts.workerLabel,
|
|
440
|
-
error: err.message
|
|
441
|
-
} });
|
|
442
|
-
this.rejectAll(err);
|
|
443
|
-
});
|
|
444
|
-
this.process.on("exit", (code) => {
|
|
445
|
-
if (code !== 0 && code !== null) {
|
|
446
|
-
this.log.error("Worker process exited", { meta: {
|
|
447
|
-
worker: this.opts.workerLabel,
|
|
448
|
-
code
|
|
449
|
-
} });
|
|
450
|
-
this.rejectAll(/* @__PURE__ */ new Error(`Worker process exited with code ${code}`));
|
|
451
|
-
}
|
|
452
|
-
this.ready = false;
|
|
453
|
-
});
|
|
454
|
-
this.process.stdout.on("data", (chunk) => {
|
|
455
|
-
this.receiveBuffer = Buffer.concat([this.receiveBuffer, chunk]);
|
|
456
|
-
this.drainResponses();
|
|
457
|
-
});
|
|
458
|
-
const config = {
|
|
459
|
-
runtime: this.opts.poolRuntime,
|
|
460
|
-
concurrency: this.opts.concurrency,
|
|
461
|
-
models: initialModels.map((m) => serializeModelConfig(m))
|
|
462
|
-
};
|
|
463
|
-
if (this.opts.tuning) {
|
|
464
|
-
config["batch_mode"] = this.opts.tuning.batchMode;
|
|
465
|
-
config["window_ms"] = this.opts.tuning.windowMs;
|
|
466
|
-
config["max_batch_size"] = this.opts.tuning.maxBatchSize;
|
|
467
|
-
config["num_streams"] = this.opts.tuning.numStreams;
|
|
468
|
-
config["intra_op_threads"] = this.opts.tuning.intraOpThreads;
|
|
364
|
+
var yolo26Reduced = (tier, res, sizes) => {
|
|
365
|
+
const name = YOLO26_TIER_NAME[tier] ?? tier.toUpperCase();
|
|
366
|
+
return [{
|
|
367
|
+
id: `yolo26${tier}-${res}`,
|
|
368
|
+
group: {
|
|
369
|
+
family: "yolo26",
|
|
370
|
+
tier,
|
|
371
|
+
resolution: res
|
|
372
|
+
},
|
|
373
|
+
name: `YOLO26 ${name} @${res}`,
|
|
374
|
+
description: `YOLO26 ${name}, reduced ${res}×${res} input — faster on ANE / hub NPU / N100 at a small-object accuracy cost`,
|
|
375
|
+
inputSize: {
|
|
376
|
+
width: res,
|
|
377
|
+
height: res
|
|
378
|
+
},
|
|
379
|
+
labels: [],
|
|
380
|
+
preprocessMode: "letterbox",
|
|
381
|
+
formats: {
|
|
382
|
+
onnx: {
|
|
383
|
+
url: hf(`objectDetection/yolo26/onnx/camstack-yolo26${tier}-${res}.onnx`),
|
|
384
|
+
sizeMB: sizes.onnx
|
|
385
|
+
},
|
|
386
|
+
coreml: {
|
|
387
|
+
url: hf(`objectDetection/yolo26/coreml/camstack-yolo26${tier}-${res}.mlpackage`),
|
|
388
|
+
sizeMB: sizes.coreml,
|
|
389
|
+
isDirectory: true,
|
|
390
|
+
files: [...MLPACKAGE_FILES],
|
|
391
|
+
runtimes: ["python"]
|
|
392
|
+
},
|
|
393
|
+
openvino: ovFormat(hf(`objectDetection/yolo26/openvino/camstack-yolo26${tier}-${res}.xml`), sizes.ovFp32)
|
|
469
394
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
this.ensureReady();
|
|
515
|
-
if (items.length === 0) return [];
|
|
516
|
-
const headerSize = 2 + items.length * 13;
|
|
517
|
-
const totalRawSize = items.reduce((sum, it) => sum + it.raw.length, 0);
|
|
518
|
-
const payload = Buffer.allocUnsafe(headerSize + totalRawSize);
|
|
519
|
-
payload[0] = modelByte;
|
|
520
|
-
payload[1] = items.length;
|
|
521
|
-
let offset = 2;
|
|
522
|
-
for (const item of items) {
|
|
523
|
-
payload.writeUInt32LE(item.width, offset);
|
|
524
|
-
payload.writeUInt32LE(item.height, offset + 4);
|
|
525
|
-
payload[offset + 8] = RAW_FMT_CODE[item.format];
|
|
526
|
-
payload.writeUInt32LE(item.raw.length, offset + 9);
|
|
527
|
-
offset += 13;
|
|
528
|
-
item.raw.copy(payload, offset);
|
|
529
|
-
offset += item.raw.length;
|
|
395
|
+
}, {
|
|
396
|
+
id: `yolo26${tier}-${res}-int8`,
|
|
397
|
+
group: {
|
|
398
|
+
family: "yolo26",
|
|
399
|
+
tier,
|
|
400
|
+
precision: "int8",
|
|
401
|
+
resolution: res
|
|
402
|
+
},
|
|
403
|
+
name: `YOLO26 ${name} @${res} (INT8)`,
|
|
404
|
+
description: `YOLO26 ${name} @${res} — OpenVINO INT8 for Intel iGPU/CPU; the N100's fastest config`,
|
|
405
|
+
inputSize: {
|
|
406
|
+
width: res,
|
|
407
|
+
height: res
|
|
408
|
+
},
|
|
409
|
+
labels: [],
|
|
410
|
+
preprocessMode: "letterbox",
|
|
411
|
+
formats: { openvino: ovFormat(hf(`objectDetection/yolo26/openvino/camstack-yolo26${tier}-${res}-int8.xml`), sizes.ovInt8) }
|
|
412
|
+
}];
|
|
413
|
+
};
|
|
414
|
+
var OBJECT_DETECTION_MODELS = [
|
|
415
|
+
{
|
|
416
|
+
id: "yolov9t",
|
|
417
|
+
legacy: true,
|
|
418
|
+
name: "YOLOv9 Tiny",
|
|
419
|
+
description: "YOLOv9 Tiny — ultra-lightweight next-gen detector",
|
|
420
|
+
inputSize: {
|
|
421
|
+
width: 640,
|
|
422
|
+
height: 640
|
|
423
|
+
},
|
|
424
|
+
labels: [],
|
|
425
|
+
preprocessMode: "letterbox",
|
|
426
|
+
formats: {
|
|
427
|
+
onnx: {
|
|
428
|
+
url: hf("objectDetection/yolov9/onnx/camstack-yolov9t.onnx"),
|
|
429
|
+
sizeMB: 8
|
|
430
|
+
},
|
|
431
|
+
coreml: {
|
|
432
|
+
url: hf("objectDetection/yolov9/coreml/camstack-yolov9t.mlpackage"),
|
|
433
|
+
sizeMB: 4,
|
|
434
|
+
isDirectory: true,
|
|
435
|
+
files: [...MLPACKAGE_FILES],
|
|
436
|
+
runtimes: ["python"]
|
|
437
|
+
},
|
|
438
|
+
openvino: ovFormat(hf("objectDetection/yolov9/openvino/camstack-yolov9t.xml"), 6)
|
|
530
439
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
this.ensureReady();
|
|
557
|
-
const payload = Buffer.from(JSON.stringify(cmd), "utf8");
|
|
558
|
-
return await this.dispatch(MSG_COMMAND, payload);
|
|
559
|
-
}
|
|
560
|
-
async dispose() {
|
|
561
|
-
const proc = this.process;
|
|
562
|
-
if (!proc) return;
|
|
563
|
-
this.process = null;
|
|
564
|
-
this.ready = false;
|
|
565
|
-
await terminateChild(proc, POOL_WORKER_TERM_GRACE_MS);
|
|
566
|
-
}
|
|
567
|
-
dispatch(msgType, payload) {
|
|
568
|
-
const reqId = this.allocRequestId();
|
|
569
|
-
return new Promise((resolve, reject) => {
|
|
570
|
-
this.pending.set(reqId, {
|
|
571
|
-
resolve,
|
|
572
|
-
reject
|
|
573
|
-
});
|
|
574
|
-
try {
|
|
575
|
-
this.writeFrame(reqId, msgType, payload);
|
|
576
|
-
} catch (err) {
|
|
577
|
-
this.pending.delete(reqId);
|
|
578
|
-
reject(err instanceof Error ? err : new Error(String(err)));
|
|
579
|
-
}
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
dispatchMulti(msgType, payloadLen, parts) {
|
|
583
|
-
const reqId = this.allocRequestId();
|
|
584
|
-
return new Promise((resolve, reject) => {
|
|
585
|
-
this.pending.set(reqId, {
|
|
586
|
-
resolve,
|
|
587
|
-
reject
|
|
588
|
-
});
|
|
589
|
-
try {
|
|
590
|
-
if (!this.process?.stdin) throw new Error("PoolWorker: not initialized");
|
|
591
|
-
const prefix = Buffer.allocUnsafe(PREFIX_LEN);
|
|
592
|
-
prefix.writeUInt32LE(5 + payloadLen, 0);
|
|
593
|
-
prefix.writeUInt32LE(reqId, 4);
|
|
594
|
-
prefix[8] = msgType;
|
|
595
|
-
this.process.stdin.write(prefix);
|
|
596
|
-
for (const part of parts) this.process.stdin.write(part);
|
|
597
|
-
} catch (err) {
|
|
598
|
-
this.pending.delete(reqId);
|
|
599
|
-
reject(err instanceof Error ? err : new Error(String(err)));
|
|
600
|
-
}
|
|
601
|
-
});
|
|
602
|
-
}
|
|
603
|
-
allocRequestId() {
|
|
604
|
-
let id = this.nextRequestId;
|
|
605
|
-
this.nextRequestId = id >= 4294967295 ? 1 : id + 1;
|
|
606
|
-
while (this.pending.has(id)) {
|
|
607
|
-
id = this.nextRequestId;
|
|
608
|
-
this.nextRequestId = id >= 4294967295 ? 1 : id + 1;
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
id: "yolov9s",
|
|
443
|
+
legacy: true,
|
|
444
|
+
name: "YOLOv9 Small",
|
|
445
|
+
description: "YOLOv9 Small — improved efficiency over YOLOv8s",
|
|
446
|
+
inputSize: {
|
|
447
|
+
width: 640,
|
|
448
|
+
height: 640
|
|
449
|
+
},
|
|
450
|
+
labels: [],
|
|
451
|
+
preprocessMode: "letterbox",
|
|
452
|
+
formats: {
|
|
453
|
+
onnx: {
|
|
454
|
+
url: hf("objectDetection/yolov9/onnx/camstack-yolov9s.onnx"),
|
|
455
|
+
sizeMB: 28
|
|
456
|
+
},
|
|
457
|
+
coreml: {
|
|
458
|
+
url: hf("objectDetection/yolov9/coreml/camstack-yolov9s.mlpackage"),
|
|
459
|
+
sizeMB: 14,
|
|
460
|
+
isDirectory: true,
|
|
461
|
+
files: [...MLPACKAGE_FILES],
|
|
462
|
+
runtimes: ["python"]
|
|
463
|
+
},
|
|
464
|
+
openvino: ovFormat(hf("objectDetection/yolov9/openvino/camstack-yolov9s.xml"), 16)
|
|
609
465
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
worker: this.opts.workerLabel,
|
|
636
|
-
reqId
|
|
637
|
-
} });
|
|
638
|
-
continue;
|
|
639
|
-
}
|
|
640
|
-
this.pending.delete(reqId);
|
|
641
|
-
try {
|
|
642
|
-
const parsed = JSON.parse(jsonBytes.toString("utf8"));
|
|
643
|
-
entry.resolve(parsed);
|
|
644
|
-
} catch (err) {
|
|
645
|
-
entry.reject(err instanceof Error ? err : new Error(String(err)));
|
|
646
|
-
}
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
id: "yolov9c",
|
|
469
|
+
legacy: true,
|
|
470
|
+
name: "YOLOv9 C",
|
|
471
|
+
description: "YOLOv9 C — high-accuracy compact model",
|
|
472
|
+
inputSize: {
|
|
473
|
+
width: 640,
|
|
474
|
+
height: 640
|
|
475
|
+
},
|
|
476
|
+
labels: [],
|
|
477
|
+
preprocessMode: "letterbox",
|
|
478
|
+
formats: {
|
|
479
|
+
onnx: {
|
|
480
|
+
url: hf("objectDetection/yolov9/onnx/camstack-yolov9c.onnx"),
|
|
481
|
+
sizeMB: 97
|
|
482
|
+
},
|
|
483
|
+
coreml: {
|
|
484
|
+
url: hf("objectDetection/yolov9/coreml/camstack-yolov9c.mlpackage"),
|
|
485
|
+
sizeMB: 48,
|
|
486
|
+
isDirectory: true,
|
|
487
|
+
files: [...MLPACKAGE_FILES],
|
|
488
|
+
runtimes: ["python"]
|
|
489
|
+
},
|
|
490
|
+
openvino: ovFormat(hf("objectDetection/yolov9/openvino/camstack-yolov9c.xml"), 49)
|
|
647
491
|
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
this.log = logger;
|
|
677
|
-
this.concurrency = Math.max(1, options.concurrency ?? 1);
|
|
678
|
-
this.tuning = options.tuning ?? null;
|
|
679
|
-
this.numWorkers = Math.max(1, options.numWorkers ?? 1);
|
|
680
|
-
}
|
|
681
|
-
/** Pid of the first worker (for legacy callers). Use `getPids()` for all. */
|
|
682
|
-
getPid() {
|
|
683
|
-
return this.workers[0]?.getPid() ?? null;
|
|
684
|
-
}
|
|
685
|
-
getPids() {
|
|
686
|
-
return this.workers.map((w) => w.getPid());
|
|
687
|
-
}
|
|
688
|
-
/**
|
|
689
|
-
* Start N pool workers in parallel. Each worker loads the same
|
|
690
|
-
* initial model set so subsequent inference calls land on a worker
|
|
691
|
-
* with the requested model already in its slot map.
|
|
692
|
-
*/
|
|
693
|
-
async initialize(initialModels = []) {
|
|
694
|
-
const scriptPath = this.resolveScriptPath();
|
|
695
|
-
for (let i = 0; i < this.numWorkers; i++) this.workers.push(new PoolWorker({
|
|
696
|
-
pythonPath: this.pythonPath,
|
|
697
|
-
scriptPath,
|
|
698
|
-
poolRuntime: this.poolRuntime,
|
|
699
|
-
concurrency: this.concurrency,
|
|
700
|
-
tuning: this.tuning,
|
|
701
|
-
logger: this.log,
|
|
702
|
-
workerLabel: `w${i}`
|
|
703
|
-
}));
|
|
704
|
-
const t0 = performance.now();
|
|
705
|
-
const results = await Promise.all(this.workers.map((w) => w.initialize(initialModels)));
|
|
706
|
-
const startupMs = Math.round(performance.now() - t0);
|
|
707
|
-
this.nextFreeIndex = initialModels.length;
|
|
708
|
-
const loadedCount = results[0]?.loadedCount ?? 0;
|
|
709
|
-
const innerWorkers = results.reduce((s, r) => s + r.workers, 0);
|
|
710
|
-
this.log.info("Pool ready", { meta: {
|
|
711
|
-
loadedCount,
|
|
712
|
-
startupMs,
|
|
713
|
-
runtime: this.poolRuntime,
|
|
714
|
-
workers: this.numWorkers,
|
|
715
|
-
pythonWorkers: innerWorkers,
|
|
716
|
-
pids: this.getPids()
|
|
717
|
-
} });
|
|
718
|
-
return {
|
|
719
|
-
startupMs,
|
|
720
|
-
loadedCount,
|
|
721
|
-
workers: this.numWorkers
|
|
722
|
-
};
|
|
723
|
-
}
|
|
724
|
-
async infer(modelIndex, jpeg) {
|
|
725
|
-
const w = this.pickWorker();
|
|
726
|
-
return this.trackDroppedResponse(await w.infer(this.encodeModelByte(modelIndex), jpeg), modelIndex);
|
|
727
|
-
}
|
|
728
|
-
async inferRaw(modelIndex, raw, width, height, format) {
|
|
729
|
-
const w = this.pickWorker();
|
|
730
|
-
return this.trackDroppedResponse(await w.inferRaw(this.encodeModelByte(modelIndex), raw, width, height, format), modelIndex);
|
|
731
|
-
}
|
|
732
|
-
async inferBatch(modelIndex, items) {
|
|
733
|
-
if (items.length > 255) throw new Error(`SharedInferencePool.inferBatch: max 255 items per call, got ${items.length}`);
|
|
734
|
-
return this.pickWorker().inferBatch(this.encodeModelByte(modelIndex), items);
|
|
735
|
-
}
|
|
736
|
-
async inferCached(modelIndex, frameId) {
|
|
737
|
-
const w = this.pickWorker();
|
|
738
|
-
return this.trackDroppedResponse(await w.inferCached(this.encodeModelByte(modelIndex), frameId), modelIndex);
|
|
739
|
-
}
|
|
740
|
-
/**
|
|
741
|
-
* Total `"dropped": true` responses seen from the Python pool — frames
|
|
742
|
-
* shed by the per-model in-flight bound under overload. Diagnostic
|
|
743
|
-
* counter (monotonic for the pool's lifetime).
|
|
744
|
-
*/
|
|
745
|
-
getDroppedResponseCount() {
|
|
746
|
-
return this.droppedResponseCount;
|
|
747
|
-
}
|
|
748
|
-
getHandle(modelIndex) {
|
|
749
|
-
return new PoolHandle(this, modelIndex);
|
|
750
|
-
}
|
|
751
|
-
async cacheFrame(raw, width, height, format) {
|
|
752
|
-
const frameId = this.nextFrameId++;
|
|
753
|
-
await Promise.all(this.workers.map((w) => w.cacheFrame(frameId, raw, width, height, format)));
|
|
754
|
-
return {
|
|
755
|
-
frameId,
|
|
756
|
-
width,
|
|
757
|
-
height
|
|
758
|
-
};
|
|
759
|
-
}
|
|
760
|
-
async uncacheFrame(frameId) {
|
|
761
|
-
await Promise.all(this.workers.map((w) => w.sendCommand({
|
|
762
|
-
cmd: "uncache_frame",
|
|
763
|
-
frameId
|
|
764
|
-
})));
|
|
765
|
-
}
|
|
766
|
-
async loadModel(index, config) {
|
|
767
|
-
const responses = await Promise.all(this.workers.map((w) => w.sendCommand({
|
|
768
|
-
cmd: "load",
|
|
769
|
-
index,
|
|
770
|
-
config: serializeModelConfig(config)
|
|
771
|
-
})));
|
|
772
|
-
for (const resp of responses) if (resp.status !== "ok") throw new Error(`Failed to load model at index ${index}: ${resp.error ?? "unknown"}`);
|
|
773
|
-
if (index >= this.nextFreeIndex) this.nextFreeIndex = index + 1;
|
|
774
|
-
return { loadMs: Math.max(...responses.map((r) => r.loadMs ?? 0)) };
|
|
775
|
-
}
|
|
776
|
-
async unloadModel(index) {
|
|
777
|
-
const responses = await Promise.all(this.workers.map((w) => w.sendCommand({
|
|
778
|
-
cmd: "unload",
|
|
779
|
-
index
|
|
780
|
-
})));
|
|
781
|
-
for (const resp of responses) if (resp.status !== "ok") throw new Error(`Failed to unload model at index ${index}: ${resp.error ?? "unknown"}`);
|
|
782
|
-
}
|
|
783
|
-
async replaceModel(index, config) {
|
|
784
|
-
const responses = await Promise.all(this.workers.map((w) => w.sendCommand({
|
|
785
|
-
cmd: "replace",
|
|
786
|
-
index,
|
|
787
|
-
config: serializeModelConfig(config)
|
|
788
|
-
})));
|
|
789
|
-
for (const resp of responses) if (resp.status !== "ok") throw new Error(`Failed to replace model at index ${index}: ${resp.error ?? "unknown"}`);
|
|
790
|
-
return { loadMs: Math.max(...responses.map((r) => r.loadMs ?? 0)) };
|
|
791
|
-
}
|
|
792
|
-
async getStatus() {
|
|
793
|
-
if (this.workers.length === 0) return [];
|
|
794
|
-
return (await this.workers[0].sendCommand({ cmd: "status" })).models ?? [];
|
|
795
|
-
}
|
|
796
|
-
allocateIndex() {
|
|
797
|
-
return this.nextFreeIndex++;
|
|
798
|
-
}
|
|
799
|
-
isReady() {
|
|
800
|
-
return this.workers.length > 0 && this.workers.every((w) => w.isReady());
|
|
801
|
-
}
|
|
802
|
-
async dispose() {
|
|
803
|
-
await Promise.all(this.workers.map((w) => w.dispose()));
|
|
804
|
-
this.workers.length = 0;
|
|
805
|
-
this.log.info("Pool process(es) terminated");
|
|
806
|
-
}
|
|
807
|
-
/**
|
|
808
|
-
* Recognise a Python overload-shed response (`"dropped": true`) on the
|
|
809
|
-
* single-frame inference paths. Previously indistinguishable from a
|
|
810
|
-
* genuine empty detection result — now counted + debug-logged so
|
|
811
|
-
* overload is measurable. The response passes through unchanged.
|
|
812
|
-
*/
|
|
813
|
-
trackDroppedResponse(result, modelIndex) {
|
|
814
|
-
if (result["dropped"] === true) {
|
|
815
|
-
this.droppedResponseCount++;
|
|
816
|
-
this.log.debug("Python pool shed frame under overload", { meta: {
|
|
817
|
-
modelIndex,
|
|
818
|
-
droppedTotal: this.droppedResponseCount
|
|
819
|
-
} });
|
|
820
|
-
}
|
|
821
|
-
return result;
|
|
822
|
-
}
|
|
823
|
-
pickWorker() {
|
|
824
|
-
if (this.workers.length === 0) throw new Error("SharedInferencePool: not initialized");
|
|
825
|
-
const idx = this.nextDispatchIdx;
|
|
826
|
-
this.nextDispatchIdx = (this.nextDispatchIdx + 1) % this.workers.length;
|
|
827
|
-
return this.workers[idx];
|
|
828
|
-
}
|
|
829
|
-
encodeModelByte(modelIndex) {
|
|
830
|
-
if (modelIndex < 0 || modelIndex > 254) throw new Error(`Model index ${modelIndex} out of range (0-254)`);
|
|
831
|
-
return modelIndex;
|
|
832
|
-
}
|
|
833
|
-
resolveScriptPath() {
|
|
834
|
-
const candidates = [
|
|
835
|
-
path$1.join(__dirname, "../../python/inference_pool.py"),
|
|
836
|
-
path$1.join(__dirname, "../python/inference_pool.py"),
|
|
837
|
-
path$1.join(__dirname, "../../../python/inference_pool.py")
|
|
838
|
-
];
|
|
839
|
-
try {
|
|
840
|
-
const pkgPath = __require.resolve("@camstack/addon-detection-pipeline/package.json");
|
|
841
|
-
candidates.unshift(path$1.join(path$1.dirname(pkgPath), "python", "inference_pool.py"));
|
|
842
|
-
} catch {}
|
|
843
|
-
const found = candidates.find((p) => fs.existsSync(p));
|
|
844
|
-
if (!found) throw new Error(`inference_pool.py not found. Searched:\n${candidates.join("\n")}`);
|
|
845
|
-
return found;
|
|
846
|
-
}
|
|
847
|
-
};
|
|
848
|
-
function serializeModelConfig(config) {
|
|
849
|
-
const result = {
|
|
850
|
-
path: config.path,
|
|
851
|
-
inputSize: config.inputSize,
|
|
852
|
-
preprocessMode: config.preprocessMode,
|
|
853
|
-
postprocessor: config.postprocessor,
|
|
854
|
-
confidence: config.confidence
|
|
855
|
-
};
|
|
856
|
-
if (config.labels) result["labels"] = config.labels;
|
|
857
|
-
if (config.charset) result["charset"] = config.charset;
|
|
858
|
-
if (config.numClasses) result["numClasses"] = config.numClasses;
|
|
859
|
-
if (config.strides) result["strides"] = config.strides;
|
|
860
|
-
if (config.maskThreshold !== void 0) result["maskThreshold"] = config.maskThreshold;
|
|
861
|
-
if (config.device) result["device"] = config.device;
|
|
862
|
-
return result;
|
|
863
|
-
}
|
|
864
|
-
//#endregion
|
|
865
|
-
//#region src/detection-pipeline/engine/step-utils.ts
|
|
866
|
-
/**
|
|
867
|
-
* Flatten a step tree into a list of enabled video steps (DFS).
|
|
868
|
-
* Audio-classifier steps are excluded — they run independently
|
|
869
|
-
* via the audio pipeline, not the video inference pool.
|
|
870
|
-
*/
|
|
871
|
-
function flattenEnabledVideoSteps(steps) {
|
|
872
|
-
const result = [];
|
|
873
|
-
const walk = (nodes) => {
|
|
874
|
-
for (const step of nodes) {
|
|
875
|
-
if (!step.enabled) continue;
|
|
876
|
-
if (step.slot === "audio-classifier") continue;
|
|
877
|
-
result.push(step);
|
|
878
|
-
if (step.children?.length) walk(step.children);
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
id: "yolo26n",
|
|
495
|
+
group: {
|
|
496
|
+
family: "yolo26",
|
|
497
|
+
tier: "n"
|
|
498
|
+
},
|
|
499
|
+
name: "YOLO26 Nano",
|
|
500
|
+
description: "YOLO26 Nano — latest generation ultra-lightweight detector with attention mechanism",
|
|
501
|
+
inputSize: {
|
|
502
|
+
width: 640,
|
|
503
|
+
height: 640
|
|
504
|
+
},
|
|
505
|
+
labels: [],
|
|
506
|
+
preprocessMode: "letterbox",
|
|
507
|
+
formats: {
|
|
508
|
+
onnx: {
|
|
509
|
+
url: hf("objectDetection/yolo26/onnx/camstack-yolo26n.onnx"),
|
|
510
|
+
sizeMB: 10
|
|
511
|
+
},
|
|
512
|
+
coreml: {
|
|
513
|
+
url: hf("objectDetection/yolo26/coreml/camstack-yolo26n.mlpackage"),
|
|
514
|
+
sizeMB: 5,
|
|
515
|
+
isDirectory: true,
|
|
516
|
+
files: [...MLPACKAGE_FILES],
|
|
517
|
+
runtimes: ["python"]
|
|
518
|
+
},
|
|
519
|
+
openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26n.xml"), 9)
|
|
879
520
|
}
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
* Steps that disappear from `newSteps` get fully unloaded (every
|
|
909
|
-
* model variant for that stepId).
|
|
910
|
-
*/
|
|
911
|
-
async applyConfig(newSteps) {
|
|
912
|
-
const enabledSteps = flattenEnabledVideoSteps(newSteps);
|
|
913
|
-
const diff = this.computeDiff(enabledSteps);
|
|
914
|
-
for (const removed of diff.removed) {
|
|
915
|
-
await this.unloadEntry(removed);
|
|
916
|
-
this.log.info("Unloading step", { meta: {
|
|
917
|
-
step: removed.stepId,
|
|
918
|
-
modelId: removed.modelId,
|
|
919
|
-
poolIndex: removed.poolIndex
|
|
920
|
-
} });
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
id: "yolo26s",
|
|
524
|
+
group: {
|
|
525
|
+
family: "yolo26",
|
|
526
|
+
tier: "s"
|
|
527
|
+
},
|
|
528
|
+
name: "YOLO26 Small",
|
|
529
|
+
description: "YOLO26 Small — balanced speed/accuracy with attention-based architecture",
|
|
530
|
+
inputSize: {
|
|
531
|
+
width: 640,
|
|
532
|
+
height: 640
|
|
533
|
+
},
|
|
534
|
+
labels: [],
|
|
535
|
+
preprocessMode: "letterbox",
|
|
536
|
+
formats: {
|
|
537
|
+
onnx: {
|
|
538
|
+
url: hf("objectDetection/yolo26/onnx/camstack-yolo26s.onnx"),
|
|
539
|
+
sizeMB: 37
|
|
540
|
+
},
|
|
541
|
+
coreml: {
|
|
542
|
+
url: hf("objectDetection/yolo26/coreml/camstack-yolo26s.mlpackage"),
|
|
543
|
+
sizeMB: 18,
|
|
544
|
+
isDirectory: true,
|
|
545
|
+
files: [...MLPACKAGE_FILES],
|
|
546
|
+
runtimes: ["python"]
|
|
547
|
+
},
|
|
548
|
+
openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26s.xml"), 36)
|
|
921
549
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
id: "yolo26m",
|
|
553
|
+
group: {
|
|
554
|
+
family: "yolo26",
|
|
555
|
+
tier: "m"
|
|
556
|
+
},
|
|
557
|
+
name: "YOLO26 Medium",
|
|
558
|
+
description: "YOLO26 Medium — high accuracy general-purpose detector",
|
|
559
|
+
inputSize: {
|
|
560
|
+
width: 640,
|
|
561
|
+
height: 640
|
|
562
|
+
},
|
|
563
|
+
labels: [],
|
|
564
|
+
preprocessMode: "letterbox",
|
|
565
|
+
formats: {
|
|
566
|
+
onnx: {
|
|
567
|
+
url: hf("objectDetection/yolo26/onnx/camstack-yolo26m.onnx"),
|
|
568
|
+
sizeMB: 78
|
|
569
|
+
},
|
|
570
|
+
coreml: {
|
|
571
|
+
url: hf("objectDetection/yolo26/coreml/camstack-yolo26m.mlpackage"),
|
|
572
|
+
sizeMB: 39,
|
|
573
|
+
isDirectory: true,
|
|
574
|
+
files: [...MLPACKAGE_FILES],
|
|
575
|
+
runtimes: ["python"]
|
|
576
|
+
},
|
|
577
|
+
openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26m.xml"), 78)
|
|
930
578
|
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
id: "yolo26l",
|
|
582
|
+
group: {
|
|
583
|
+
family: "yolo26",
|
|
584
|
+
tier: "l"
|
|
585
|
+
},
|
|
586
|
+
name: "YOLO26 Large",
|
|
587
|
+
description: "YOLO26 Large — maximum accuracy for demanding deployments",
|
|
588
|
+
inputSize: {
|
|
589
|
+
width: 640,
|
|
590
|
+
height: 640
|
|
591
|
+
},
|
|
592
|
+
labels: [],
|
|
593
|
+
preprocessMode: "letterbox",
|
|
594
|
+
formats: {
|
|
595
|
+
onnx: {
|
|
596
|
+
url: hf("objectDetection/yolo26/onnx/camstack-yolo26l.onnx"),
|
|
597
|
+
sizeMB: 95
|
|
598
|
+
},
|
|
599
|
+
coreml: {
|
|
600
|
+
url: hf("objectDetection/yolo26/coreml/camstack-yolo26l.mlpackage"),
|
|
601
|
+
sizeMB: 48,
|
|
602
|
+
isDirectory: true,
|
|
603
|
+
files: [...MLPACKAGE_FILES],
|
|
604
|
+
runtimes: ["python"]
|
|
605
|
+
},
|
|
606
|
+
openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26l.xml"), 95)
|
|
940
607
|
}
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
/**
|
|
967
|
-
* Pool index for a given (stepId, modelId). Without `modelId`,
|
|
968
|
-
* returns the active model's index. Used by the inference fast
|
|
969
|
-
* paths that bypass `getHandle` and call `pool.inferBatch` directly.
|
|
970
|
-
*/
|
|
971
|
-
getPoolIndex(stepId, modelId) {
|
|
972
|
-
const entry = this.resolve(stepId, modelId);
|
|
973
|
-
if (!entry) return null;
|
|
974
|
-
this.touch(entry);
|
|
975
|
-
return entry.poolIndex;
|
|
976
|
-
}
|
|
977
|
-
/**
|
|
978
|
-
* Direct access to the underlying pool — exposed so the provider can
|
|
979
|
-
* call `inferBatch` without re-fetching individual handles. Pool
|
|
980
|
-
* lifetime is owned by `EngineFactory`, so callers must not dispose.
|
|
981
|
-
*/
|
|
982
|
-
getPool() {
|
|
983
|
-
return this.pool;
|
|
984
|
-
}
|
|
985
|
-
/**
|
|
986
|
-
* Snapshot of every (stepId, modelId, poolIndex) currently resident.
|
|
987
|
-
* Used by the `Warm instances` UI tab + cap-level `listLoadedEngines`
|
|
988
|
-
* to surface both the runtime model and the warm extras.
|
|
989
|
-
*/
|
|
990
|
-
getLoadedSteps() {
|
|
991
|
-
const out = [];
|
|
992
|
-
for (const [stepId, models] of this.loaded) {
|
|
993
|
-
const activeModelId = this.activeByStep.get(stepId);
|
|
994
|
-
for (const entry of models.values()) out.push({
|
|
995
|
-
stepId: entry.stepId,
|
|
996
|
-
modelId: entry.modelId,
|
|
997
|
-
poolIndex: entry.poolIndex,
|
|
998
|
-
active: entry.modelId === activeModelId
|
|
999
|
-
});
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
id: "yolo26x",
|
|
611
|
+
legacy: true,
|
|
612
|
+
name: "YOLO26 XLarge",
|
|
613
|
+
description: "YOLO26 XLarge — highest accuracy, attention-based architecture",
|
|
614
|
+
inputSize: {
|
|
615
|
+
width: 640,
|
|
616
|
+
height: 640
|
|
617
|
+
},
|
|
618
|
+
labels: [],
|
|
619
|
+
preprocessMode: "letterbox",
|
|
620
|
+
formats: {
|
|
621
|
+
onnx: {
|
|
622
|
+
url: hf("objectDetection/yolo26/onnx/camstack-yolo26x.onnx"),
|
|
623
|
+
sizeMB: 213
|
|
624
|
+
},
|
|
625
|
+
coreml: {
|
|
626
|
+
url: hf("objectDetection/yolo26/coreml/camstack-yolo26x.mlpackage"),
|
|
627
|
+
sizeMB: 107,
|
|
628
|
+
isDirectory: true,
|
|
629
|
+
files: [...MLPACKAGE_FILES],
|
|
630
|
+
runtimes: ["python"]
|
|
631
|
+
},
|
|
632
|
+
openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26x.xml"), 213)
|
|
1000
633
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
id: "scrypted-yolov9t-relu",
|
|
637
|
+
legacy: true,
|
|
638
|
+
name: "YOLOv9 Tiny ReLU",
|
|
639
|
+
description: "YOLOv9t ReLU — optimized for int8 quantization",
|
|
640
|
+
inputSize: {
|
|
641
|
+
width: 320,
|
|
642
|
+
height: 320
|
|
643
|
+
},
|
|
644
|
+
labels: [],
|
|
645
|
+
preprocessMode: "letterbox",
|
|
646
|
+
formats: {
|
|
647
|
+
onnx: {
|
|
648
|
+
url: hfScrypted("onnx/scrypted_yolov9t_relu/scrypted_yolov9t_relu.onnx"),
|
|
649
|
+
sizeMB: 8.1
|
|
650
|
+
},
|
|
651
|
+
coreml: {
|
|
652
|
+
url: hfScrypted("coreml/scrypted_yolov9t_relu/scrypted_yolov9t_relu.mlpackage"),
|
|
653
|
+
sizeMB: 4,
|
|
654
|
+
isDirectory: true,
|
|
655
|
+
files: [...MLPACKAGE_FILES],
|
|
656
|
+
runtimes: ["python"]
|
|
657
|
+
},
|
|
658
|
+
openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9t_relu.xml"), 6)
|
|
1016
659
|
}
|
|
1017
|
-
}
|
|
1018
|
-
/**
|
|
1019
|
-
* Load `(stepId, modelId)` into the pool if not already resident.
|
|
1020
|
-
* Honors `maxModelsPerStep` — evicts a non-active LRU variant first
|
|
1021
|
-
* when the cap would otherwise be exceeded.
|
|
1022
|
-
*/
|
|
1023
|
-
async ensureLoaded(stepId, modelId) {
|
|
1024
|
-
let perStep = this.loaded.get(stepId);
|
|
1025
|
-
if (!perStep) {
|
|
1026
|
-
perStep = /* @__PURE__ */ new Map();
|
|
1027
|
-
this.loaded.set(stepId, perStep);
|
|
1028
|
-
}
|
|
1029
|
-
const existing = perStep.get(modelId);
|
|
1030
|
-
if (existing) {
|
|
1031
|
-
this.touch(existing);
|
|
1032
|
-
return existing;
|
|
1033
|
-
}
|
|
1034
|
-
while (perStep.size >= this.maxModelsPerStep) {
|
|
1035
|
-
const evicted = this.pickEvictionTarget(stepId);
|
|
1036
|
-
if (!evicted) break;
|
|
1037
|
-
await this.unloadEntry(evicted);
|
|
1038
|
-
this.log.info("LRU evicted warm model", { meta: {
|
|
1039
|
-
step: stepId,
|
|
1040
|
-
modelId: evicted.modelId,
|
|
1041
|
-
poolIndex: evicted.poolIndex,
|
|
1042
|
-
cap: this.maxModelsPerStep
|
|
1043
|
-
} });
|
|
1044
|
-
}
|
|
1045
|
-
const index = this.pool.allocateIndex();
|
|
1046
|
-
const config = this.buildPoolConfig(stepId, modelId);
|
|
1047
|
-
this.log.info("Loading step variant", { meta: {
|
|
1048
|
-
step: stepId,
|
|
1049
|
-
modelId,
|
|
1050
|
-
poolIndex: index
|
|
1051
|
-
} });
|
|
1052
|
-
const { loadMs } = await this.pool.loadModel(index, config);
|
|
1053
|
-
this.log.info("Step variant loaded", { meta: {
|
|
1054
|
-
step: stepId,
|
|
1055
|
-
modelId,
|
|
1056
|
-
poolIndex: index,
|
|
1057
|
-
loadMs
|
|
1058
|
-
} });
|
|
1059
|
-
const entry = {
|
|
1060
|
-
stepId,
|
|
1061
|
-
modelId,
|
|
1062
|
-
poolIndex: index,
|
|
1063
|
-
lruTick: ++this.lruClock
|
|
1064
|
-
};
|
|
1065
|
-
perStep.set(modelId, entry);
|
|
1066
|
-
return entry;
|
|
1067
|
-
}
|
|
1068
|
-
async unloadEntry(entry) {
|
|
1069
|
-
await this.pool.unloadModel(entry.poolIndex);
|
|
1070
|
-
const perStep = this.loaded.get(entry.stepId);
|
|
1071
|
-
perStep?.delete(entry.modelId);
|
|
1072
|
-
if (perStep && perStep.size === 0) {
|
|
1073
|
-
this.loaded.delete(entry.stepId);
|
|
1074
|
-
this.activeByStep.delete(entry.stepId);
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
/** Select the LRU non-active entry for a step. Returns null when only the active one is present. */
|
|
1078
|
-
pickEvictionTarget(stepId) {
|
|
1079
|
-
const perStep = this.loaded.get(stepId);
|
|
1080
|
-
if (!perStep) return null;
|
|
1081
|
-
const activeModelId = this.activeByStep.get(stepId);
|
|
1082
|
-
let oldest = null;
|
|
1083
|
-
for (const entry of perStep.values()) {
|
|
1084
|
-
if (entry.modelId === activeModelId) continue;
|
|
1085
|
-
if (!oldest || entry.lruTick < oldest.lruTick) oldest = entry;
|
|
1086
|
-
}
|
|
1087
|
-
return oldest;
|
|
1088
|
-
}
|
|
1089
|
-
/** Trim the step down to `maxModelsPerStep` entries, evicting LRU non-actives. */
|
|
1090
|
-
async enforceLruCap(stepId) {
|
|
1091
|
-
const perStep = this.loaded.get(stepId);
|
|
1092
|
-
if (!perStep) return;
|
|
1093
|
-
while (perStep.size > this.maxModelsPerStep) {
|
|
1094
|
-
const evicted = this.pickEvictionTarget(stepId);
|
|
1095
|
-
if (!evicted) break;
|
|
1096
|
-
await this.unloadEntry(evicted);
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
resolve(stepId, modelId) {
|
|
1100
|
-
const perStep = this.loaded.get(stepId);
|
|
1101
|
-
if (!perStep) return null;
|
|
1102
|
-
const targetModelId = modelId ?? this.activeByStep.get(stepId);
|
|
1103
|
-
if (!targetModelId) return null;
|
|
1104
|
-
return perStep.get(targetModelId) ?? null;
|
|
1105
|
-
}
|
|
1106
|
-
resolveOrThrow(stepId, modelId) {
|
|
1107
|
-
const entry = this.resolve(stepId, modelId);
|
|
1108
|
-
if (!entry) throw new Error(`Step "${stepId}"${modelId ? ` (model "${modelId}")` : ""} is not loaded in the inference pool`);
|
|
1109
|
-
return entry;
|
|
1110
|
-
}
|
|
1111
|
-
touch(entry) {
|
|
1112
|
-
entry.lruTick = ++this.lruClock;
|
|
1113
|
-
}
|
|
1114
|
-
/** Compute diff between currently-active steps and desired steps. */
|
|
1115
|
-
computeDiff(desiredSteps) {
|
|
1116
|
-
const desiredMap = /* @__PURE__ */ new Map();
|
|
1117
|
-
for (const step of desiredSteps) desiredMap.set(step.addonId, step);
|
|
1118
|
-
const added = [];
|
|
1119
|
-
const removed = [];
|
|
1120
|
-
const modelChanged = [];
|
|
1121
|
-
const unchanged = [];
|
|
1122
|
-
for (const [stepId, activeModelId] of this.activeByStep) {
|
|
1123
|
-
const activeEntry = this.loaded.get(stepId)?.get(activeModelId);
|
|
1124
|
-
if (!activeEntry) continue;
|
|
1125
|
-
const desired = desiredMap.get(stepId);
|
|
1126
|
-
if (!desired) {
|
|
1127
|
-
const perStep = this.loaded.get(stepId);
|
|
1128
|
-
if (perStep) for (const entry of perStep.values()) removed.push(entry);
|
|
1129
|
-
} else if (desired.modelId !== activeModelId) modelChanged.push({
|
|
1130
|
-
step: desired,
|
|
1131
|
-
oldModelId: activeModelId
|
|
1132
|
-
});
|
|
1133
|
-
else unchanged.push(activeEntry);
|
|
1134
|
-
}
|
|
1135
|
-
for (const [stepId, step] of desiredMap) if (!this.activeByStep.has(stepId)) added.push(step);
|
|
1136
|
-
return {
|
|
1137
|
-
added,
|
|
1138
|
-
removed,
|
|
1139
|
-
modelChanged,
|
|
1140
|
-
unchanged
|
|
1141
|
-
};
|
|
1142
|
-
}
|
|
1143
|
-
};
|
|
1144
|
-
//#endregion
|
|
1145
|
-
//#region src/detection-pipeline/registry/model-catalogs.ts
|
|
1146
|
-
var HF_REPO = "camstack/camstack-models";
|
|
1147
|
-
var HF_SCRYPTED = "scrypted/plugin-models";
|
|
1148
|
-
var hf = (path) => hfModelUrl(HF_REPO, path);
|
|
1149
|
-
var hfScrypted = (path) => hfModelUrl(HF_SCRYPTED, path);
|
|
1150
|
-
/**
|
|
1151
|
-
* Build an OpenVINO format entry (always python runtime).
|
|
1152
|
-
*
|
|
1153
|
-
* OpenVINO IR is a two-file bundle: a `.xml` topology + a sibling `.bin`
|
|
1154
|
-
* weights file with the same basename. We declare the `.bin` in `files` so
|
|
1155
|
-
* the (format-agnostic) downloader fetches it alongside the `.xml` — without
|
|
1156
|
-
* the weights, OpenVINO compile fails with "Empty weights data in bin file".
|
|
1157
|
-
* A plain `.onnx` run through the OpenVINO runtime (e.g. yamnet) has no
|
|
1158
|
-
* sibling, so none is added.
|
|
1159
|
-
*/
|
|
1160
|
-
var ovFormat = (url, sizeMB) => {
|
|
1161
|
-
const base = url.split("/").pop() ?? "";
|
|
1162
|
-
const files = base.endsWith(".xml") ? [base.replace(/\.xml$/, ".bin")] : void 0;
|
|
1163
|
-
return {
|
|
1164
|
-
url,
|
|
1165
|
-
sizeMB,
|
|
1166
|
-
runtimes: ["python"],
|
|
1167
|
-
...files ? { files } : {}
|
|
1168
|
-
};
|
|
1169
|
-
};
|
|
1170
|
-
/**
|
|
1171
|
-
* Build a precision-variant catalog entry (OpenVINO-only) derived from a base
|
|
1172
|
-
* detection model. fp16 halves the weights (Intel iGPU/NPU sweet spot); int8 is
|
|
1173
|
-
* NNCF post-training-quantized (~4× smaller, fastest on CPU/iGPU at a small
|
|
1174
|
-
* accuracy cost). The IRs live next to the base `.xml` on HF as
|
|
1175
|
-
* `camstack-<id>-<precision>.xml`. Lets an operator scale the model to the node
|
|
1176
|
-
* (e.g. yolo26x-int8 on a 265K, yolo26n-int8 on an N100).
|
|
1177
|
-
*/
|
|
1178
|
-
var ovPrecisionVariant = (baseId, ovDir, baseName, precision, sizeMB, legacy = false) => ({
|
|
1179
|
-
id: `${baseId}-${precision}`,
|
|
1180
|
-
name: `${baseName} (${precision.toUpperCase()})`,
|
|
1181
|
-
description: `${baseName} — OpenVINO ${precision.toUpperCase()} variant for Intel iGPU/NPU; scale by hardware`,
|
|
1182
|
-
inputSize: {
|
|
1183
|
-
width: 640,
|
|
1184
|
-
height: 640
|
|
1185
660
|
},
|
|
1186
|
-
labels: [],
|
|
1187
|
-
preprocessMode: "letterbox",
|
|
1188
|
-
formats: { openvino: ovFormat(hf(`${ovDir}/camstack-${baseId}-${precision}.xml`), sizeMB) },
|
|
1189
|
-
...legacy ? { legacy: true } : {},
|
|
1190
|
-
...precision === "int8" ? { group: {
|
|
1191
|
-
family: baseId.slice(0, -1),
|
|
1192
|
-
tier: baseId.slice(-1),
|
|
1193
|
-
precision: "int8"
|
|
1194
|
-
} } : {}
|
|
1195
|
-
});
|
|
1196
|
-
var MLPACKAGE_FILES = [
|
|
1197
|
-
"Manifest.json",
|
|
1198
|
-
"Data/com.apple.CoreML/model.mlmodel",
|
|
1199
|
-
"Data/com.apple.CoreML/weights/weight.bin"
|
|
1200
|
-
];
|
|
1201
|
-
var OBJECT_DETECTION_MODELS = [
|
|
1202
661
|
{
|
|
1203
|
-
id: "
|
|
662
|
+
id: "scrypted-yolov9s-relu",
|
|
1204
663
|
legacy: true,
|
|
1205
|
-
name: "YOLOv9
|
|
1206
|
-
description: "
|
|
664
|
+
name: "YOLOv9 Small ReLU",
|
|
665
|
+
description: "YOLOv9s ReLU — improved efficiency, int8 ready",
|
|
1207
666
|
inputSize: {
|
|
1208
|
-
width:
|
|
1209
|
-
height:
|
|
667
|
+
width: 320,
|
|
668
|
+
height: 320
|
|
1210
669
|
},
|
|
1211
670
|
labels: [],
|
|
1212
671
|
preprocessMode: "letterbox",
|
|
1213
672
|
formats: {
|
|
1214
673
|
onnx: {
|
|
1215
|
-
url:
|
|
1216
|
-
sizeMB:
|
|
674
|
+
url: hfScrypted("onnx/scrypted_yolov9s_relu/scrypted_yolov9s_relu.onnx"),
|
|
675
|
+
sizeMB: 27.9
|
|
1217
676
|
},
|
|
1218
677
|
coreml: {
|
|
1219
|
-
url:
|
|
1220
|
-
sizeMB:
|
|
678
|
+
url: hfScrypted("coreml/scrypted_yolov9s_relu/scrypted_yolov9s_relu.mlpackage"),
|
|
679
|
+
sizeMB: 14,
|
|
1221
680
|
isDirectory: true,
|
|
1222
681
|
files: [...MLPACKAGE_FILES],
|
|
1223
682
|
runtimes: ["python"]
|
|
1224
683
|
},
|
|
1225
|
-
openvino: ovFormat(hf("objectDetection/yolov9/openvino/
|
|
684
|
+
openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9s_relu.xml"), 16)
|
|
1226
685
|
}
|
|
1227
686
|
},
|
|
1228
687
|
{
|
|
1229
|
-
id: "
|
|
688
|
+
id: "scrypted-yolov9c-relu",
|
|
1230
689
|
legacy: true,
|
|
1231
|
-
name: "YOLOv9
|
|
1232
|
-
description: "
|
|
690
|
+
name: "YOLOv9 C ReLU",
|
|
691
|
+
description: "YOLOv9c ReLU — high-accuracy, int8 ready",
|
|
1233
692
|
inputSize: {
|
|
1234
|
-
width:
|
|
1235
|
-
height:
|
|
693
|
+
width: 320,
|
|
694
|
+
height: 320
|
|
1236
695
|
},
|
|
1237
696
|
labels: [],
|
|
1238
697
|
preprocessMode: "letterbox",
|
|
1239
698
|
formats: {
|
|
1240
699
|
onnx: {
|
|
1241
|
-
url:
|
|
1242
|
-
sizeMB:
|
|
700
|
+
url: hfScrypted("onnx/scrypted_yolov9c_relu/scrypted_yolov9c_relu.onnx"),
|
|
701
|
+
sizeMB: 96.9
|
|
1243
702
|
},
|
|
1244
703
|
coreml: {
|
|
1245
|
-
url:
|
|
1246
|
-
sizeMB:
|
|
704
|
+
url: hfScrypted("coreml/scrypted_yolov9c_relu/scrypted_yolov9c_relu.mlpackage"),
|
|
705
|
+
sizeMB: 48,
|
|
1247
706
|
isDirectory: true,
|
|
1248
707
|
files: [...MLPACKAGE_FILES],
|
|
1249
708
|
runtimes: ["python"]
|
|
1250
709
|
},
|
|
1251
|
-
openvino: ovFormat(hf("objectDetection/yolov9/openvino/
|
|
710
|
+
openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9c_relu.xml"), 49)
|
|
1252
711
|
}
|
|
1253
712
|
},
|
|
1254
713
|
{
|
|
1255
|
-
id: "
|
|
714
|
+
id: "scrypted-yolov9m-relu",
|
|
1256
715
|
legacy: true,
|
|
1257
|
-
name: "YOLOv9
|
|
1258
|
-
description: "
|
|
716
|
+
name: "YOLOv9 M ReLU",
|
|
717
|
+
description: "YOLOv9m ReLU — medium, int8 ready",
|
|
1259
718
|
inputSize: {
|
|
1260
|
-
width:
|
|
1261
|
-
height:
|
|
719
|
+
width: 320,
|
|
720
|
+
height: 320
|
|
1262
721
|
},
|
|
1263
722
|
labels: [],
|
|
1264
723
|
preprocessMode: "letterbox",
|
|
1265
724
|
formats: {
|
|
1266
725
|
onnx: {
|
|
1267
|
-
url:
|
|
1268
|
-
sizeMB:
|
|
726
|
+
url: hfScrypted("onnx/scrypted_yolov9m_relu/scrypted_yolov9m_relu.onnx"),
|
|
727
|
+
sizeMB: 76.6
|
|
1269
728
|
},
|
|
1270
729
|
coreml: {
|
|
1271
|
-
url:
|
|
1272
|
-
sizeMB:
|
|
730
|
+
url: hfScrypted("coreml/scrypted_yolov9m_relu/scrypted_yolov9m_relu.mlpackage"),
|
|
731
|
+
sizeMB: 38,
|
|
1273
732
|
isDirectory: true,
|
|
1274
733
|
files: [...MLPACKAGE_FILES],
|
|
1275
734
|
runtimes: ["python"]
|
|
1276
735
|
},
|
|
1277
|
-
openvino: ovFormat(hf("objectDetection/yolov9/openvino/
|
|
736
|
+
openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9m_relu.xml"), 38)
|
|
1278
737
|
}
|
|
1279
738
|
},
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
739
|
+
ovPrecisionVariant("yolov9t", "objectDetection/yolov9/openvino", "YOLOv9 Tiny", "fp16", 5, true),
|
|
740
|
+
ovPrecisionVariant("yolov9t", "objectDetection/yolov9/openvino", "YOLOv9 Tiny", "int8", 3, true),
|
|
741
|
+
ovPrecisionVariant("yolov9s", "objectDetection/yolov9/openvino", "YOLOv9 Small", "fp16", 15, true),
|
|
742
|
+
ovPrecisionVariant("yolov9s", "objectDetection/yolov9/openvino", "YOLOv9 Small", "int8", 8, true),
|
|
743
|
+
ovPrecisionVariant("yolo26n", "objectDetection/yolo26/openvino", "YOLO26 Nano", "fp16", 5, true),
|
|
744
|
+
ovPrecisionVariant("yolo26n", "objectDetection/yolo26/openvino", "YOLO26 Nano", "int8", 3),
|
|
745
|
+
ovPrecisionVariant("yolo26s", "objectDetection/yolo26/openvino", "YOLO26 Small", "fp16", 19, true),
|
|
746
|
+
ovPrecisionVariant("yolo26s", "objectDetection/yolo26/openvino", "YOLO26 Small", "int8", 10),
|
|
747
|
+
ovPrecisionVariant("yolo26m", "objectDetection/yolo26/openvino", "YOLO26 Medium", "fp16", 41, true),
|
|
748
|
+
ovPrecisionVariant("yolo26m", "objectDetection/yolo26/openvino", "YOLO26 Medium", "int8", 21),
|
|
749
|
+
ovPrecisionVariant("yolo26l", "objectDetection/yolo26/openvino", "YOLO26 Large", "fp16", 50, true),
|
|
750
|
+
ovPrecisionVariant("yolo26l", "objectDetection/yolo26/openvino", "YOLO26 Large", "int8", 25),
|
|
751
|
+
...yolo26Reduced("n", 320, {
|
|
752
|
+
onnx: 10.47,
|
|
753
|
+
coreml: 5.05,
|
|
754
|
+
ovFp32: 10.05,
|
|
755
|
+
ovInt8: 3.29
|
|
756
|
+
}),
|
|
757
|
+
...yolo26Reduced("n", 256, {
|
|
758
|
+
onnx: 10.47,
|
|
759
|
+
coreml: 5.05,
|
|
760
|
+
ovFp32: 10.05,
|
|
761
|
+
ovInt8: 3.29
|
|
762
|
+
}),
|
|
763
|
+
...yolo26Reduced("s", 320, {
|
|
764
|
+
onnx: 38.82,
|
|
765
|
+
coreml: 19.23,
|
|
766
|
+
ovFp32: 38.4,
|
|
767
|
+
ovInt8: 10.43
|
|
768
|
+
}),
|
|
769
|
+
...yolo26Reduced("s", 256, {
|
|
770
|
+
onnx: 38.82,
|
|
771
|
+
coreml: 19.23,
|
|
772
|
+
ovFp32: 38.4,
|
|
773
|
+
ovInt8: 10.43
|
|
774
|
+
}),
|
|
775
|
+
...yolo26Reduced("m", 320, {
|
|
776
|
+
onnx: 83.31,
|
|
777
|
+
coreml: 41.07,
|
|
778
|
+
ovFp32: 82.09,
|
|
779
|
+
ovInt8: 21.47
|
|
780
|
+
}),
|
|
781
|
+
...yolo26Reduced("m", 256, {
|
|
782
|
+
onnx: 83.31,
|
|
783
|
+
coreml: 41.07,
|
|
784
|
+
ovFp32: 82.09,
|
|
785
|
+
ovInt8: 21.47
|
|
786
|
+
}),
|
|
787
|
+
...yolo26Reduced("l", 320, {
|
|
788
|
+
onnx: 101.74,
|
|
789
|
+
coreml: 49.97,
|
|
790
|
+
ovFp32: 99.85,
|
|
791
|
+
ovInt8: 26.31
|
|
792
|
+
}),
|
|
793
|
+
...yolo26Reduced("l", 256, {
|
|
794
|
+
onnx: 101.74,
|
|
795
|
+
coreml: 49.97,
|
|
796
|
+
ovFp32: 99.85,
|
|
797
|
+
ovInt8: 26.31
|
|
798
|
+
}),
|
|
799
|
+
ovPrecisionVariant("yolo26x", "objectDetection/yolo26/openvino", "YOLO26 XLarge", "fp16", 112, true),
|
|
800
|
+
ovPrecisionVariant("yolo26x", "objectDetection/yolo26/openvino", "YOLO26 XLarge", "int8", 56, true)
|
|
801
|
+
];
|
|
802
|
+
var FACE_DETECTION_MODELS = [{
|
|
803
|
+
id: "scrfd-2.5g",
|
|
804
|
+
name: "SCRFD 2.5G",
|
|
805
|
+
description: "SCRFD 2.5G — balanced face detection model",
|
|
806
|
+
inputSize: {
|
|
807
|
+
width: 640,
|
|
808
|
+
height: 640
|
|
1308
809
|
},
|
|
1309
|
-
{
|
|
1310
|
-
id: "
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
810
|
+
labels: [{
|
|
811
|
+
id: "face",
|
|
812
|
+
name: "Face"
|
|
813
|
+
}],
|
|
814
|
+
preprocessMode: "letterbox",
|
|
815
|
+
formats: {
|
|
816
|
+
onnx: {
|
|
817
|
+
url: hf("faceDetection/scrfd/onnx/camstack-scrfd-2.5g.onnx"),
|
|
818
|
+
sizeMB: 3.1
|
|
1314
819
|
},
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
820
|
+
coreml: {
|
|
821
|
+
url: hf("faceDetection/scrfd/coreml/camstack-scrfd-2.5g.mlpackage"),
|
|
822
|
+
sizeMB: 1.7,
|
|
823
|
+
isDirectory: true,
|
|
824
|
+
files: [...MLPACKAGE_FILES],
|
|
825
|
+
runtimes: ["python"]
|
|
1320
826
|
},
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
sizeMB: 18,
|
|
1331
|
-
isDirectory: true,
|
|
1332
|
-
files: [...MLPACKAGE_FILES],
|
|
1333
|
-
runtimes: ["python"]
|
|
1334
|
-
},
|
|
1335
|
-
openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26s.xml"), 36)
|
|
1336
|
-
}
|
|
827
|
+
openvino: ovFormat(hf("faceDetection/scrfd/openvino/camstack-scrfd-2.5g.xml"), 1.8)
|
|
828
|
+
}
|
|
829
|
+
}, {
|
|
830
|
+
id: "scrypted-yolov9t-face",
|
|
831
|
+
name: "YOLOv9t Face ReLU",
|
|
832
|
+
description: "YOLOv9t face detection — YOLO-based, fast, 320x320 ReLU",
|
|
833
|
+
inputSize: {
|
|
834
|
+
width: 320,
|
|
835
|
+
height: 320
|
|
1337
836
|
},
|
|
1338
|
-
{
|
|
1339
|
-
id: "
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
837
|
+
labels: [{
|
|
838
|
+
id: "face",
|
|
839
|
+
name: "Face"
|
|
840
|
+
}],
|
|
841
|
+
preprocessMode: "letterbox",
|
|
842
|
+
formats: {
|
|
843
|
+
onnx: {
|
|
844
|
+
url: hfScrypted("onnx/scrypted_yolov9t_relu_face/scrypted_yolov9t_relu_face.onnx"),
|
|
845
|
+
sizeMB: 8.1
|
|
1343
846
|
},
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
847
|
+
coreml: {
|
|
848
|
+
url: hfScrypted("coreml/scrypted_yolov9t_relu_face/scrypted_yolov9t_relu_face.mlpackage"),
|
|
849
|
+
sizeMB: 4,
|
|
850
|
+
isDirectory: true,
|
|
851
|
+
files: [...MLPACKAGE_FILES],
|
|
852
|
+
runtimes: ["python"]
|
|
1349
853
|
},
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
isDirectory: true,
|
|
1361
|
-
files: [...MLPACKAGE_FILES],
|
|
1362
|
-
runtimes: ["python"]
|
|
1363
|
-
},
|
|
1364
|
-
openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26m.xml"), 78)
|
|
1365
|
-
}
|
|
854
|
+
openvino: ovFormat(hf("faceDetection/scrypted-yolov9-face/openvino/scrypted_yolov9t_relu_face.xml"), 6)
|
|
855
|
+
}
|
|
856
|
+
}];
|
|
857
|
+
var FACE_EMBEDDING_MODELS = [{
|
|
858
|
+
id: "arcface-r100",
|
|
859
|
+
name: "ArcFace R100",
|
|
860
|
+
description: "ArcFace ResNet-100 — high-accuracy face recognition embeddings (512-d)",
|
|
861
|
+
inputSize: {
|
|
862
|
+
width: 112,
|
|
863
|
+
height: 112
|
|
1366
864
|
},
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
865
|
+
inputLayout: "nhwc",
|
|
866
|
+
labels: [{
|
|
867
|
+
id: "embedding",
|
|
868
|
+
name: "Face Embedding"
|
|
869
|
+
}],
|
|
870
|
+
preprocessMode: "resize",
|
|
871
|
+
faceAlignment: true,
|
|
872
|
+
formats: {
|
|
873
|
+
onnx: {
|
|
874
|
+
url: hf("faceRecognition/arcface/onnx/camstack-arcface-arcface.onnx"),
|
|
875
|
+
sizeMB: 130
|
|
1372
876
|
},
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
877
|
+
coreml: {
|
|
878
|
+
url: hf("faceRecognition/arcface/coreml/camstack-arcface-r100.mlpackage"),
|
|
879
|
+
sizeMB: 65,
|
|
880
|
+
isDirectory: true,
|
|
881
|
+
files: [...MLPACKAGE_FILES],
|
|
882
|
+
runtimes: ["python"]
|
|
1378
883
|
},
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
sizeMB: 48,
|
|
1389
|
-
isDirectory: true,
|
|
1390
|
-
files: [...MLPACKAGE_FILES],
|
|
1391
|
-
runtimes: ["python"]
|
|
1392
|
-
},
|
|
1393
|
-
openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26l.xml"), 95)
|
|
1394
|
-
}
|
|
884
|
+
openvino: ovFormat(hf("faceRecognition/arcface/openvino/camstack-arcface-r100.xml"), 65)
|
|
885
|
+
}
|
|
886
|
+
}, {
|
|
887
|
+
id: "inception-resnet-v1",
|
|
888
|
+
name: "Inception ResNet V1",
|
|
889
|
+
description: "FaceNet-style face recognition embeddings (512-d) — hosted on plugin-models HF repo",
|
|
890
|
+
inputSize: {
|
|
891
|
+
width: 160,
|
|
892
|
+
height: 160
|
|
1395
893
|
},
|
|
1396
|
-
{
|
|
1397
|
-
id: "
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
894
|
+
labels: [{
|
|
895
|
+
id: "embedding",
|
|
896
|
+
name: "Face Embedding"
|
|
897
|
+
}],
|
|
898
|
+
preprocessMode: "resize",
|
|
899
|
+
formats: {
|
|
900
|
+
onnx: {
|
|
901
|
+
url: hfScrypted("onnx/inception_resnet_v1/inception_resnet_v1.onnx"),
|
|
902
|
+
sizeMB: 89.6
|
|
1404
903
|
},
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
904
|
+
coreml: {
|
|
905
|
+
url: hfScrypted("coreml/inception_resnet_v1/inception_resnet_v1.mlpackage"),
|
|
906
|
+
sizeMB: 45,
|
|
907
|
+
isDirectory: true,
|
|
908
|
+
files: [...MLPACKAGE_FILES],
|
|
909
|
+
runtimes: ["python"]
|
|
910
|
+
},
|
|
911
|
+
openvino: ovFormat(hf("faceRecognition/inception-resnet-v1/openvino/camstack-inception-resnet-v1.xml"), 45)
|
|
912
|
+
}
|
|
913
|
+
}];
|
|
914
|
+
var PLATE_DETECTION_MODELS = [{
|
|
915
|
+
id: "yolov8n-plate",
|
|
916
|
+
name: "YOLOv8 Nano — License Plate",
|
|
917
|
+
description: "YOLOv8 Nano fine-tuned for license plate detection",
|
|
918
|
+
inputSize: {
|
|
919
|
+
width: 640,
|
|
920
|
+
height: 640
|
|
921
|
+
},
|
|
922
|
+
labels: [{
|
|
923
|
+
id: "plate",
|
|
924
|
+
name: "License Plate"
|
|
925
|
+
}],
|
|
926
|
+
preprocessMode: "letterbox",
|
|
927
|
+
formats: {
|
|
928
|
+
onnx: {
|
|
929
|
+
url: hf("plateDetection/yolov8-plate/onnx/camstack-yolov8n-plate.onnx"),
|
|
930
|
+
sizeMB: 12
|
|
931
|
+
},
|
|
932
|
+
coreml: {
|
|
933
|
+
url: hf("plateDetection/yolov8-plate/coreml/camstack-yolov8n-plate.mlpackage"),
|
|
934
|
+
sizeMB: 5.9,
|
|
935
|
+
isDirectory: true,
|
|
936
|
+
files: [...MLPACKAGE_FILES],
|
|
937
|
+
runtimes: ["python"]
|
|
938
|
+
},
|
|
939
|
+
openvino: ovFormat(hf("plateDetection/yolov8-plate/openvino/camstack-yolov8n-plate.xml"), 6.1)
|
|
940
|
+
}
|
|
941
|
+
}];
|
|
942
|
+
var PLATE_OCR_MODELS = [{
|
|
943
|
+
id: "vgg-english-g2",
|
|
944
|
+
name: "VGG English G2",
|
|
945
|
+
description: "EasyOCR VGG English G2 — text recognition for license plates and scene text",
|
|
946
|
+
inputSize: {
|
|
947
|
+
width: 384,
|
|
948
|
+
height: 64
|
|
949
|
+
},
|
|
950
|
+
labels: [{
|
|
951
|
+
id: "text",
|
|
952
|
+
name: "Plate Text"
|
|
953
|
+
}],
|
|
954
|
+
preprocessMode: "resize",
|
|
955
|
+
formats: {
|
|
956
|
+
onnx: {
|
|
957
|
+
url: hfScrypted("onnx/vgg_english_g2/vgg_english_g2.onnx"),
|
|
958
|
+
sizeMB: 14.4
|
|
959
|
+
},
|
|
960
|
+
coreml: {
|
|
961
|
+
url: hfScrypted("coreml/vgg_english_g2/vgg_english_g2.mlpackage"),
|
|
962
|
+
sizeMB: 11.2,
|
|
963
|
+
isDirectory: true,
|
|
964
|
+
files: [...MLPACKAGE_FILES],
|
|
965
|
+
runtimes: ["python"]
|
|
966
|
+
},
|
|
967
|
+
openvino: ovFormat(hf("plateRecognition/vgg_english_g2/openvino/vgg_english_g2.xml"), 7.2)
|
|
968
|
+
}
|
|
969
|
+
}];
|
|
970
|
+
var ANIMAL_CLASSIFIER_MODELS = [{
|
|
971
|
+
id: "animals-10",
|
|
972
|
+
name: "Animal Classifier (10)",
|
|
973
|
+
description: "ViT-based animal type classifier — cat, cow, dog, dolphin, eagle, panda, horse, monkey, sheep, spider",
|
|
974
|
+
inputSize: {
|
|
975
|
+
width: 224,
|
|
976
|
+
height: 224
|
|
977
|
+
},
|
|
978
|
+
inputNormalization: "imagenet",
|
|
979
|
+
labels: [{
|
|
980
|
+
id: "animal-type",
|
|
981
|
+
name: "Animal Type"
|
|
982
|
+
}],
|
|
983
|
+
preprocessMode: "resize",
|
|
984
|
+
formats: {
|
|
985
|
+
onnx: {
|
|
986
|
+
url: hf("animalClassification/animals-10/onnx/camstack-animals-10.onnx"),
|
|
987
|
+
sizeMB: 328
|
|
988
|
+
},
|
|
989
|
+
coreml: {
|
|
990
|
+
url: hf("animalClassification/animals-10/coreml/camstack-animals-10.mlpackage"),
|
|
991
|
+
sizeMB: 164,
|
|
992
|
+
isDirectory: true,
|
|
993
|
+
files: [...MLPACKAGE_FILES],
|
|
994
|
+
runtimes: ["python"]
|
|
995
|
+
},
|
|
996
|
+
openvino: ovFormat(hf("animalClassification/animals-10/openvino/camstack-animals-10.xml"), 164)
|
|
997
|
+
}
|
|
998
|
+
}];
|
|
999
|
+
var BIRD_CLASSIFIER_MODELS = [{
|
|
1000
|
+
id: "bird-nabirds-404",
|
|
1001
|
+
name: "NABirds (404 species)",
|
|
1002
|
+
description: "ResNet50 trained on NABirds — 404 North American species",
|
|
1003
|
+
inputSize: {
|
|
1004
|
+
width: 224,
|
|
1005
|
+
height: 224
|
|
1006
|
+
},
|
|
1007
|
+
inputNormalization: "imagenet",
|
|
1008
|
+
labels: [{
|
|
1009
|
+
id: "species",
|
|
1010
|
+
name: "Bird Species"
|
|
1011
|
+
}],
|
|
1012
|
+
preprocessMode: "resize",
|
|
1013
|
+
formats: {
|
|
1014
|
+
onnx: {
|
|
1015
|
+
url: hf("animalClassification/bird-nabirds/onnx/camstack-bird-nabirds-404.onnx"),
|
|
1016
|
+
sizeMB: 93
|
|
1017
|
+
},
|
|
1018
|
+
coreml: {
|
|
1019
|
+
url: hf("animalClassification/bird-nabirds/coreml/camstack-bird-nabirds-404.mlpackage"),
|
|
1020
|
+
sizeMB: 47,
|
|
1021
|
+
isDirectory: true,
|
|
1022
|
+
files: [...MLPACKAGE_FILES],
|
|
1023
|
+
runtimes: ["python"]
|
|
1024
|
+
},
|
|
1025
|
+
openvino: ovFormat(hf("animalClassification/bird-nabirds/openvino/camstack-bird-nabirds-404.xml"), 47)
|
|
1026
|
+
},
|
|
1027
|
+
extraFiles: [{
|
|
1028
|
+
url: hf("animalClassification/bird-nabirds/onnx/camstack-bird-nabirds-404-labels.json"),
|
|
1029
|
+
filename: "camstack-bird-nabirds-404-labels.json",
|
|
1030
|
+
sizeMB: .02
|
|
1031
|
+
}]
|
|
1032
|
+
}];
|
|
1033
|
+
var VEHICLE_CLASSIFIER_MODELS = [{
|
|
1034
|
+
id: "vehicle-type-efficientnet",
|
|
1035
|
+
name: "Vehicle Type (EfficientNet)",
|
|
1036
|
+
description: "EfficientNet-B4 vehicle make/model/year classifier — 8,949 classes from VMMRdb",
|
|
1037
|
+
inputSize: {
|
|
1038
|
+
width: 380,
|
|
1039
|
+
height: 380
|
|
1040
|
+
},
|
|
1041
|
+
inputNormalization: "imagenet",
|
|
1042
|
+
labels: [{
|
|
1043
|
+
id: "vehicle-type",
|
|
1044
|
+
name: "Vehicle Type"
|
|
1045
|
+
}],
|
|
1046
|
+
preprocessMode: "resize",
|
|
1047
|
+
formats: {
|
|
1048
|
+
onnx: {
|
|
1049
|
+
url: hf("vehicleClassification/efficientnet/onnx/camstack-vehicle-type-efficientnet.onnx"),
|
|
1050
|
+
sizeMB: 135
|
|
1051
|
+
},
|
|
1052
|
+
coreml: {
|
|
1053
|
+
url: hf("vehicleClassification/efficientnet/coreml/camstack-vehicle-type-efficientnet.mlpackage"),
|
|
1054
|
+
sizeMB: 10,
|
|
1055
|
+
isDirectory: true,
|
|
1056
|
+
files: [...MLPACKAGE_FILES],
|
|
1057
|
+
runtimes: ["python"]
|
|
1058
|
+
},
|
|
1059
|
+
openvino: ovFormat(hf("vehicleClassification/efficientnet/openvino/camstack-vehicle-type-efficientnet.xml"), 68)
|
|
1060
|
+
},
|
|
1061
|
+
extraFiles: [{
|
|
1062
|
+
url: hf("vehicleClassification/efficientnet/camstack-vehicle-type-labels.json"),
|
|
1063
|
+
filename: "camstack-vehicle-type-labels.json",
|
|
1064
|
+
sizeMB: .2
|
|
1065
|
+
}]
|
|
1066
|
+
}];
|
|
1067
|
+
var SEGMENTATION_REFINER_MODELS = [{
|
|
1068
|
+
id: "u2netp",
|
|
1069
|
+
name: "U2-Net Portable",
|
|
1070
|
+
description: "U2-Net-P — ultra-lightweight salient object segmentation (4.7 MB)",
|
|
1071
|
+
inputSize: {
|
|
1072
|
+
width: 320,
|
|
1073
|
+
height: 320
|
|
1074
|
+
},
|
|
1075
|
+
labels: [{
|
|
1076
|
+
id: "mask",
|
|
1077
|
+
name: "Segmentation Mask"
|
|
1078
|
+
}],
|
|
1079
|
+
preprocessMode: "resize",
|
|
1080
|
+
formats: {
|
|
1081
|
+
onnx: {
|
|
1082
|
+
url: hf("segmentationRefiner/u2netp/onnx/camstack-u2netp.onnx"),
|
|
1083
|
+
sizeMB: 5
|
|
1084
|
+
},
|
|
1085
|
+
coreml: {
|
|
1086
|
+
url: hf("segmentationRefiner/u2netp/coreml/camstack-u2netp.mlpackage"),
|
|
1087
|
+
sizeMB: 3,
|
|
1088
|
+
isDirectory: true,
|
|
1089
|
+
files: [...MLPACKAGE_FILES],
|
|
1090
|
+
runtimes: ["python"]
|
|
1091
|
+
},
|
|
1092
|
+
openvino: ovFormat(hf("segmentationRefiner/u2netp/openvino/camstack-u2netp.xml"), 2.5)
|
|
1093
|
+
}
|
|
1094
|
+
}];
|
|
1095
|
+
var INSTANCE_SEGMENTATION_MODELS = [
|
|
1096
|
+
{
|
|
1097
|
+
id: "yolo26n-seg",
|
|
1098
|
+
name: "YOLO26 Nano Seg",
|
|
1099
|
+
description: "YOLO26 Nano Segmentation — ultra-lightweight instance segmentation with masks",
|
|
1100
|
+
inputSize: {
|
|
1101
|
+
width: 640,
|
|
1102
|
+
height: 640
|
|
1103
|
+
},
|
|
1104
|
+
labels: [],
|
|
1105
|
+
preprocessMode: "letterbox",
|
|
1106
|
+
formats: {
|
|
1107
|
+
onnx: {
|
|
1108
|
+
url: hf("segmentation/yolo26-seg/onnx/camstack-yolo26n-seg.onnx"),
|
|
1109
|
+
sizeMB: 11
|
|
1110
|
+
},
|
|
1111
|
+
coreml: {
|
|
1112
|
+
url: hf("segmentation/yolo26-seg/coreml/camstack-yolo26n-seg.mlpackage"),
|
|
1113
|
+
sizeMB: 6,
|
|
1114
|
+
isDirectory: true,
|
|
1115
|
+
files: [...MLPACKAGE_FILES],
|
|
1116
|
+
runtimes: ["python"]
|
|
1117
|
+
},
|
|
1118
|
+
openvino: ovFormat(hf("segmentation/yolo26-seg/openvino/camstack-yolo26n-seg.xml"), 11)
|
|
1119
|
+
}
|
|
1120
|
+
},
|
|
1121
|
+
{
|
|
1122
|
+
id: "yolo26s-seg",
|
|
1123
|
+
name: "YOLO26 Small Seg",
|
|
1124
|
+
description: "YOLO26 Small Segmentation — balanced instance segmentation",
|
|
1125
|
+
inputSize: {
|
|
1126
|
+
width: 640,
|
|
1127
|
+
height: 640
|
|
1128
|
+
},
|
|
1129
|
+
labels: [],
|
|
1130
|
+
preprocessMode: "letterbox",
|
|
1131
|
+
formats: {
|
|
1132
|
+
onnx: {
|
|
1133
|
+
url: hf("segmentation/yolo26-seg/onnx/camstack-yolo26s-seg.onnx"),
|
|
1134
|
+
sizeMB: 40
|
|
1135
|
+
},
|
|
1136
|
+
coreml: {
|
|
1137
|
+
url: hf("segmentation/yolo26-seg/coreml/camstack-yolo26s-seg.mlpackage"),
|
|
1138
|
+
sizeMB: 20,
|
|
1139
|
+
isDirectory: true,
|
|
1140
|
+
files: [...MLPACKAGE_FILES],
|
|
1141
|
+
runtimes: ["python"]
|
|
1142
|
+
},
|
|
1143
|
+
openvino: ovFormat(hf("segmentation/yolo26-seg/openvino/camstack-yolo26s-seg.xml"), 40)
|
|
1144
|
+
}
|
|
1145
|
+
},
|
|
1146
|
+
{
|
|
1147
|
+
id: "yolo26m-seg",
|
|
1148
|
+
name: "YOLO26 Medium Seg",
|
|
1149
|
+
description: "YOLO26 Medium Segmentation — high-accuracy instance segmentation",
|
|
1150
|
+
inputSize: {
|
|
1151
|
+
width: 640,
|
|
1152
|
+
height: 640
|
|
1153
|
+
},
|
|
1154
|
+
labels: [],
|
|
1155
|
+
preprocessMode: "letterbox",
|
|
1156
|
+
formats: {
|
|
1157
|
+
onnx: {
|
|
1158
|
+
url: hf("segmentation/yolo26-seg/onnx/camstack-yolo26m-seg.onnx"),
|
|
1159
|
+
sizeMB: 90
|
|
1160
|
+
},
|
|
1161
|
+
coreml: {
|
|
1162
|
+
url: hf("segmentation/yolo26-seg/coreml/camstack-yolo26m-seg.mlpackage"),
|
|
1163
|
+
sizeMB: 45,
|
|
1164
|
+
isDirectory: true,
|
|
1165
|
+
files: [...MLPACKAGE_FILES],
|
|
1166
|
+
runtimes: ["python"]
|
|
1167
|
+
},
|
|
1168
|
+
openvino: ovFormat(hf("segmentation/yolo26-seg/openvino/camstack-yolo26m-seg.xml"), 90)
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
];
|
|
1172
|
+
var CLIP_EMBEDDING_MODELS = [
|
|
1173
|
+
{
|
|
1174
|
+
id: "mobileclip-s0",
|
|
1175
|
+
name: "MobileCLIP S0",
|
|
1176
|
+
description: "MobileCLIP S0 — Apple ultra-fast CLIP vision encoder, 512-dim, 256×256 (40 MB ONNX)",
|
|
1177
|
+
inputSize: {
|
|
1178
|
+
width: 256,
|
|
1179
|
+
height: 256
|
|
1180
|
+
},
|
|
1181
|
+
labels: [{
|
|
1182
|
+
id: "embedding",
|
|
1183
|
+
name: "CLIP Embedding"
|
|
1184
|
+
}],
|
|
1185
|
+
preprocessMode: "resize",
|
|
1186
|
+
inputNormalization: "none",
|
|
1187
|
+
formats: {
|
|
1188
|
+
onnx: {
|
|
1189
|
+
url: hf("clip/mobileclip-s0/onnx/camstack-mobileclip-s0-vision.onnx"),
|
|
1190
|
+
sizeMB: 40
|
|
1191
|
+
},
|
|
1192
|
+
openvino: ovFormat(hf("clip/mobileclip-s0/openvino/camstack-mobileclip-s0-vision.xml"), 25),
|
|
1193
|
+
coreml: {
|
|
1194
|
+
url: hf("clip/mobileclip-s0/coreml/camstack-mobileclip-s0-vision.mlpackage"),
|
|
1195
|
+
sizeMB: 30,
|
|
1196
|
+
isDirectory: true,
|
|
1197
|
+
files: [...MLPACKAGE_FILES],
|
|
1198
|
+
runtimes: ["python"]
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
},
|
|
1202
|
+
{
|
|
1203
|
+
id: "mobileclip-s1",
|
|
1204
|
+
name: "MobileCLIP S1",
|
|
1205
|
+
description: "MobileCLIP S1 — Apple balanced CLIP vision encoder, 512-dim, 256×256 (90 MB ONNX)",
|
|
1206
|
+
inputSize: {
|
|
1207
|
+
width: 256,
|
|
1208
|
+
height: 256
|
|
1209
|
+
},
|
|
1210
|
+
labels: [{
|
|
1211
|
+
id: "embedding",
|
|
1212
|
+
name: "CLIP Embedding"
|
|
1213
|
+
}],
|
|
1214
|
+
preprocessMode: "resize",
|
|
1215
|
+
inputNormalization: "none",
|
|
1216
|
+
formats: {
|
|
1217
|
+
onnx: {
|
|
1218
|
+
url: hf("clip/mobileclip-s1/onnx/camstack-mobileclip-s1-vision.onnx"),
|
|
1219
|
+
sizeMB: 90
|
|
1220
|
+
},
|
|
1221
|
+
openvino: ovFormat(hf("clip/mobileclip-s1/openvino/camstack-mobileclip-s1-vision.xml"), 55),
|
|
1222
|
+
coreml: {
|
|
1223
|
+
url: hf("clip/mobileclip-s1/coreml/camstack-mobileclip-s1-vision.mlpackage"),
|
|
1224
|
+
sizeMB: 65,
|
|
1225
|
+
isDirectory: true,
|
|
1226
|
+
files: [...MLPACKAGE_FILES],
|
|
1227
|
+
runtimes: ["python"]
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
},
|
|
1231
|
+
{
|
|
1232
|
+
id: "mobileclip-s2",
|
|
1233
|
+
name: "MobileCLIP S2",
|
|
1234
|
+
description: "MobileCLIP S2 — Apple high-accuracy CLIP vision encoder, 512-dim, 256×256 (150 MB ONNX)",
|
|
1235
|
+
inputSize: {
|
|
1236
|
+
width: 256,
|
|
1237
|
+
height: 256
|
|
1238
|
+
},
|
|
1239
|
+
labels: [{
|
|
1240
|
+
id: "embedding",
|
|
1241
|
+
name: "CLIP Embedding"
|
|
1242
|
+
}],
|
|
1243
|
+
preprocessMode: "resize",
|
|
1244
|
+
inputNormalization: "none",
|
|
1245
|
+
formats: {
|
|
1246
|
+
onnx: {
|
|
1247
|
+
url: hf("clip/mobileclip-s2/onnx/camstack-mobileclip-s2-vision.onnx"),
|
|
1248
|
+
sizeMB: 150
|
|
1249
|
+
},
|
|
1250
|
+
openvino: ovFormat(hf("clip/mobileclip-s2/openvino/camstack-mobileclip-s2-vision.xml"), 90),
|
|
1251
|
+
coreml: {
|
|
1252
|
+
url: hf("clip/mobileclip-s2/coreml/camstack-mobileclip-s2-vision.mlpackage"),
|
|
1253
|
+
sizeMB: 110,
|
|
1254
|
+
isDirectory: true,
|
|
1255
|
+
files: [...MLPACKAGE_FILES],
|
|
1256
|
+
runtimes: ["python"]
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
];
|
|
1261
|
+
var AUDIO_CLASSIFIER_MODELS = [{
|
|
1262
|
+
id: "yamnet-onnx",
|
|
1263
|
+
name: "YAMNet",
|
|
1264
|
+
description: "Google YAMNet — 521-class audio event classifier (3.2 MB ONNX, runs on any platform)",
|
|
1265
|
+
inputSize: {
|
|
1266
|
+
width: 1,
|
|
1267
|
+
height: 16e3
|
|
1268
|
+
},
|
|
1269
|
+
labels: [],
|
|
1270
|
+
preprocessMode: "resize",
|
|
1271
|
+
formats: {
|
|
1272
|
+
onnx: {
|
|
1273
|
+
url: hf("audioClassification/yamnet/onnx/camstack-yamnet.onnx"),
|
|
1274
|
+
sizeMB: 3.2
|
|
1275
|
+
},
|
|
1276
|
+
openvino: ovFormat(hf("audioClassification/yamnet/openvino/camstack-yamnet.xml"), 3.2),
|
|
1277
|
+
coreml: {
|
|
1278
|
+
url: hf("audioClassification/yamnet/onnx/camstack-yamnet.onnx"),
|
|
1279
|
+
sizeMB: 3.2,
|
|
1280
|
+
runtimes: ["python"]
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}, {
|
|
1284
|
+
id: "apple-soundanalysis",
|
|
1285
|
+
name: "Apple SoundAnalysis",
|
|
1286
|
+
description: "macOS built-in — 303 sound categories, Neural Engine accelerated, zero download",
|
|
1287
|
+
inputSize: {
|
|
1288
|
+
width: 1,
|
|
1289
|
+
height: 16e3
|
|
1290
|
+
},
|
|
1291
|
+
labels: [],
|
|
1292
|
+
preprocessMode: "resize",
|
|
1293
|
+
formats: { coreml: {
|
|
1294
|
+
url: "",
|
|
1295
|
+
sizeMB: 0
|
|
1296
|
+
} }
|
|
1297
|
+
}];
|
|
1298
|
+
//#endregion
|
|
1299
|
+
//#region src/detection-pipeline/registry/step-definitions.ts
|
|
1300
|
+
var ANIMAL_10_LABELS = [
|
|
1301
|
+
"cat",
|
|
1302
|
+
"cow",
|
|
1303
|
+
"dog",
|
|
1304
|
+
"dolphin",
|
|
1305
|
+
"eagle",
|
|
1306
|
+
"giant panda",
|
|
1307
|
+
"horse",
|
|
1308
|
+
"monkey",
|
|
1309
|
+
"sheep",
|
|
1310
|
+
"spider"
|
|
1311
|
+
];
|
|
1312
|
+
var VGG_G2_CHARSET = ["", ..."0123456789!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split("")];
|
|
1313
|
+
var PipelineStepBase = class {
|
|
1314
|
+
definition;
|
|
1315
|
+
constructor(definition) {
|
|
1316
|
+
this.definition = definition;
|
|
1317
|
+
}
|
|
1318
|
+
getConfigSchema() {
|
|
1319
|
+
return [];
|
|
1320
|
+
}
|
|
1321
|
+
};
|
|
1322
|
+
var ObjectDetectionStep = class {
|
|
1323
|
+
definition = {
|
|
1324
|
+
id: "object-detection",
|
|
1325
|
+
name: "Object Detection",
|
|
1326
|
+
slot: "detector",
|
|
1327
|
+
postprocessor: "yolo",
|
|
1328
|
+
extractMode: "full-frame",
|
|
1329
|
+
inputClasses: null,
|
|
1330
|
+
outputClasses: [
|
|
1331
|
+
"person",
|
|
1332
|
+
"vehicle",
|
|
1333
|
+
"animal"
|
|
1334
|
+
],
|
|
1335
|
+
models: [...OBJECT_DETECTION_MODELS],
|
|
1336
|
+
defaultModelId: "yolo26n",
|
|
1337
|
+
defaultConfidence: .5,
|
|
1338
|
+
labels: COCO_80_LABELS.map((l) => l.id),
|
|
1339
|
+
classMap: COCO_TO_MACRO
|
|
1340
|
+
};
|
|
1341
|
+
getConfigSchema() {
|
|
1342
|
+
return [
|
|
1343
|
+
{
|
|
1344
|
+
type: "multiselect",
|
|
1345
|
+
key: "enabledMacroClasses",
|
|
1346
|
+
label: "Enabled Classes",
|
|
1347
|
+
description: "Which macro classes to detect (empty = all three).",
|
|
1348
|
+
options: [
|
|
1349
|
+
{
|
|
1350
|
+
value: "person",
|
|
1351
|
+
label: "Person"
|
|
1352
|
+
},
|
|
1353
|
+
{
|
|
1354
|
+
value: "vehicle",
|
|
1355
|
+
label: "Vehicle"
|
|
1356
|
+
},
|
|
1357
|
+
{
|
|
1358
|
+
value: "animal",
|
|
1359
|
+
label: "Animal"
|
|
1360
|
+
}
|
|
1361
|
+
],
|
|
1362
|
+
default: [
|
|
1363
|
+
"person",
|
|
1364
|
+
"vehicle",
|
|
1365
|
+
"animal"
|
|
1366
|
+
]
|
|
1367
|
+
},
|
|
1368
|
+
{
|
|
1369
|
+
type: "slider",
|
|
1370
|
+
key: "minConfidencePerson",
|
|
1371
|
+
label: "Min Confidence — Person",
|
|
1372
|
+
description: "Drop person predictions below this score.",
|
|
1373
|
+
min: 0,
|
|
1374
|
+
max: 1,
|
|
1375
|
+
step: .05,
|
|
1376
|
+
default: .5,
|
|
1377
|
+
showValue: true
|
|
1378
|
+
},
|
|
1379
|
+
{
|
|
1380
|
+
type: "slider",
|
|
1381
|
+
key: "minConfidenceVehicle",
|
|
1382
|
+
label: "Min Confidence — Vehicle",
|
|
1383
|
+
description: "Drop vehicle predictions below this score.",
|
|
1384
|
+
min: 0,
|
|
1385
|
+
max: 1,
|
|
1386
|
+
step: .05,
|
|
1387
|
+
default: .5,
|
|
1388
|
+
showValue: true
|
|
1389
|
+
},
|
|
1390
|
+
{
|
|
1391
|
+
type: "slider",
|
|
1392
|
+
key: "minConfidenceAnimal",
|
|
1393
|
+
label: "Min Confidence — Animal",
|
|
1394
|
+
description: "Drop animal predictions below this score.",
|
|
1395
|
+
min: 0,
|
|
1396
|
+
max: 1,
|
|
1397
|
+
step: .05,
|
|
1398
|
+
default: .5,
|
|
1399
|
+
showValue: true
|
|
1400
|
+
}
|
|
1401
|
+
];
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
var AudioClassifierStep = class {
|
|
1405
|
+
definition = {
|
|
1406
|
+
id: "audio-classifier",
|
|
1407
|
+
name: "Audio Classifier",
|
|
1408
|
+
slot: "audio-classifier",
|
|
1409
|
+
postprocessor: "yamnet",
|
|
1410
|
+
extractMode: "full-frame",
|
|
1411
|
+
inputClasses: null,
|
|
1412
|
+
outputClasses: ["audio"],
|
|
1413
|
+
models: [...AUDIO_CLASSIFIER_MODELS],
|
|
1414
|
+
defaultModelId: "yamnet-onnx",
|
|
1415
|
+
defaultModelIdByFormat: { coreml: "apple-soundanalysis" },
|
|
1416
|
+
enabledByDefault: true,
|
|
1417
|
+
defaultConfidence: .3
|
|
1418
|
+
};
|
|
1419
|
+
getConfigSchema() {
|
|
1420
|
+
return [{
|
|
1421
|
+
type: "multiselect",
|
|
1422
|
+
key: "enabledAudioClasses",
|
|
1423
|
+
label: "Audio Classes",
|
|
1424
|
+
description: "Which audio macro classes to surface (empty = all).",
|
|
1425
|
+
options: AUDIO_MACRO_LABELS.map((l) => ({
|
|
1426
|
+
value: l.id,
|
|
1427
|
+
label: `${l.icon ?? ""} ${l.name}`.trim()
|
|
1428
|
+
})),
|
|
1429
|
+
default: [
|
|
1430
|
+
"scream",
|
|
1431
|
+
"crying",
|
|
1432
|
+
"dog",
|
|
1433
|
+
"alarm",
|
|
1434
|
+
"doorbell",
|
|
1435
|
+
"glass_breaking",
|
|
1436
|
+
"gunshot",
|
|
1437
|
+
"siren",
|
|
1438
|
+
"footsteps"
|
|
1439
|
+
]
|
|
1440
|
+
}, {
|
|
1441
|
+
type: "slider",
|
|
1442
|
+
key: "minConfidence",
|
|
1443
|
+
label: "Min Confidence",
|
|
1444
|
+
description: "Drop predictions whose score is below this threshold.",
|
|
1445
|
+
default: .3,
|
|
1446
|
+
min: 0,
|
|
1447
|
+
max: 1,
|
|
1448
|
+
step: .05,
|
|
1449
|
+
showValue: true
|
|
1450
|
+
}];
|
|
1451
|
+
}
|
|
1452
|
+
};
|
|
1453
|
+
var AnimalClassifierStep = class {
|
|
1454
|
+
definition = {
|
|
1455
|
+
id: "animal-classifier",
|
|
1456
|
+
name: "Animal Classifier",
|
|
1457
|
+
slot: "classifier",
|
|
1458
|
+
postprocessor: "softmax",
|
|
1459
|
+
extractMode: "crop-roi",
|
|
1460
|
+
inputClasses: ["animal"],
|
|
1461
|
+
outputClasses: ["animal-type"],
|
|
1462
|
+
models: [...ANIMAL_CLASSIFIER_MODELS],
|
|
1463
|
+
defaultModelId: "animals-10",
|
|
1464
|
+
enabledByDefault: false,
|
|
1465
|
+
defaultConfidence: .2,
|
|
1466
|
+
labels: [...ANIMAL_10_LABELS]
|
|
1467
|
+
};
|
|
1468
|
+
getConfigSchema() {
|
|
1469
|
+
return [{
|
|
1470
|
+
type: "multiselect",
|
|
1471
|
+
key: "enabledClasses",
|
|
1472
|
+
label: "Species",
|
|
1473
|
+
description: "Which animal species to keep (empty = all). The classifier still scores every label but only the selected classes are returned on the detection.",
|
|
1474
|
+
options: ANIMAL_10_LABELS.map((l) => ({
|
|
1475
|
+
value: l,
|
|
1476
|
+
label: l
|
|
1477
|
+
})),
|
|
1478
|
+
default: []
|
|
1479
|
+
}, {
|
|
1480
|
+
type: "slider",
|
|
1481
|
+
key: "minConfidence",
|
|
1482
|
+
label: "Min Confidence",
|
|
1483
|
+
description: "Drop classifications whose score is below this threshold.",
|
|
1484
|
+
default: this.definition.defaultConfidence,
|
|
1485
|
+
min: 0,
|
|
1486
|
+
max: 1,
|
|
1487
|
+
step: .05,
|
|
1488
|
+
showValue: true
|
|
1489
|
+
}];
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
var ClassifierWithMinConfidence = class extends PipelineStepBase {
|
|
1493
|
+
getConfigSchema() {
|
|
1494
|
+
return [{
|
|
1495
|
+
type: "slider",
|
|
1496
|
+
key: "minConfidence",
|
|
1497
|
+
label: "Min Confidence",
|
|
1498
|
+
description: "Drop classifications whose score is below this threshold.",
|
|
1499
|
+
default: this.definition.defaultConfidence,
|
|
1500
|
+
min: 0,
|
|
1501
|
+
max: 1,
|
|
1502
|
+
step: .05,
|
|
1503
|
+
showValue: true
|
|
1504
|
+
}];
|
|
1505
|
+
}
|
|
1506
|
+
};
|
|
1507
|
+
var STEP_FACE_DETECTION = new PipelineStepBase({
|
|
1508
|
+
id: "face-detection",
|
|
1509
|
+
name: "Face Detection",
|
|
1510
|
+
slot: "cropper",
|
|
1511
|
+
postprocessor: "scrfd",
|
|
1512
|
+
extractMode: "crop-roi",
|
|
1513
|
+
inputClasses: ["person"],
|
|
1514
|
+
outputClasses: ["face"],
|
|
1515
|
+
models: [...FACE_DETECTION_MODELS],
|
|
1516
|
+
defaultModelId: "scrfd-2.5g",
|
|
1517
|
+
defaultConfidence: .5,
|
|
1518
|
+
defaultMinParentScore: .7
|
|
1519
|
+
});
|
|
1520
|
+
var STEP_FACE_EMBEDDING = new PipelineStepBase({
|
|
1521
|
+
id: "face-embedding",
|
|
1522
|
+
name: "Face Embedding",
|
|
1523
|
+
slot: "classifier",
|
|
1524
|
+
postprocessor: "arcface",
|
|
1525
|
+
extractMode: "crop-roi",
|
|
1526
|
+
inputClasses: ["face"],
|
|
1527
|
+
outputClasses: ["identity"],
|
|
1528
|
+
models: [...FACE_EMBEDDING_MODELS],
|
|
1529
|
+
defaultModelId: "arcface-r100",
|
|
1530
|
+
defaultConfidence: 0
|
|
1531
|
+
});
|
|
1532
|
+
var STEP_CLIP_EMBEDDING = new PipelineStepBase({
|
|
1533
|
+
id: "clip-embedding",
|
|
1534
|
+
name: "CLIP Embedding",
|
|
1535
|
+
slot: "classifier",
|
|
1536
|
+
postprocessor: "clip",
|
|
1537
|
+
extractMode: "crop-roi",
|
|
1538
|
+
inputClasses: null,
|
|
1539
|
+
outputClasses: ["embedding"],
|
|
1540
|
+
models: [...CLIP_EMBEDDING_MODELS],
|
|
1541
|
+
defaultModelId: "mobileclip-s1",
|
|
1542
|
+
enabledByDefault: false,
|
|
1543
|
+
defaultConfidence: 0,
|
|
1544
|
+
defaultMinParentScore: .4,
|
|
1545
|
+
group: "CLIP"
|
|
1546
|
+
});
|
|
1547
|
+
var STEP_PLATE_DETECTION = new PipelineStepBase({
|
|
1548
|
+
id: "plate-detection",
|
|
1549
|
+
name: "Plate Detection",
|
|
1550
|
+
slot: "cropper",
|
|
1551
|
+
postprocessor: "yolo",
|
|
1552
|
+
extractMode: "crop-roi",
|
|
1553
|
+
inputClasses: ["vehicle"],
|
|
1554
|
+
outputClasses: ["plate"],
|
|
1555
|
+
models: [...PLATE_DETECTION_MODELS],
|
|
1556
|
+
defaultModelId: "yolov8n-plate",
|
|
1557
|
+
defaultConfidence: .5,
|
|
1558
|
+
defaultMinParentScore: .7,
|
|
1559
|
+
labels: ["plate"]
|
|
1560
|
+
});
|
|
1561
|
+
var STEP_PLATE_OCR = new PipelineStepBase({
|
|
1562
|
+
id: "plate-ocr",
|
|
1563
|
+
name: "Plate OCR",
|
|
1564
|
+
slot: "classifier",
|
|
1565
|
+
postprocessor: "ctc",
|
|
1566
|
+
extractMode: "crop-roi",
|
|
1567
|
+
inputClasses: ["plate"],
|
|
1568
|
+
outputClasses: ["plate-text"],
|
|
1569
|
+
models: [...PLATE_OCR_MODELS],
|
|
1570
|
+
defaultModelId: "vgg-english-g2",
|
|
1571
|
+
defaultConfidence: 0,
|
|
1572
|
+
charset: [...VGG_G2_CHARSET]
|
|
1573
|
+
});
|
|
1574
|
+
var STEP_BIRD_CLASSIFIER = new ClassifierWithMinConfidence({
|
|
1575
|
+
id: "bird-classifier",
|
|
1576
|
+
name: "Bird Classifier",
|
|
1577
|
+
slot: "classifier",
|
|
1578
|
+
postprocessor: "softmax",
|
|
1579
|
+
extractMode: "crop-roi",
|
|
1580
|
+
inputClasses: ["animal"],
|
|
1581
|
+
outputClasses: ["species"],
|
|
1582
|
+
models: [...BIRD_CLASSIFIER_MODELS],
|
|
1583
|
+
defaultModelId: "bird-nabirds-404",
|
|
1584
|
+
enabledByDefault: false,
|
|
1585
|
+
defaultConfidence: .2
|
|
1586
|
+
});
|
|
1587
|
+
var STEP_VEHICLE_CLASSIFIER = new ClassifierWithMinConfidence({
|
|
1588
|
+
id: "vehicle-classifier",
|
|
1589
|
+
name: "Vehicle Classifier",
|
|
1590
|
+
slot: "classifier",
|
|
1591
|
+
postprocessor: "softmax",
|
|
1592
|
+
extractMode: "crop-roi",
|
|
1593
|
+
inputClasses: ["vehicle"],
|
|
1594
|
+
outputClasses: ["vehicle-type"],
|
|
1595
|
+
models: [...VEHICLE_CLASSIFIER_MODELS],
|
|
1596
|
+
defaultModelId: "vehicle-type-efficientnet",
|
|
1597
|
+
enabledByDefault: false,
|
|
1598
|
+
defaultConfidence: .3
|
|
1599
|
+
});
|
|
1600
|
+
var STEP_SEGMENTATION = new PipelineStepBase({
|
|
1601
|
+
id: "segmentation",
|
|
1602
|
+
name: "Segmentation",
|
|
1603
|
+
slot: "refiner",
|
|
1604
|
+
postprocessor: "saliency",
|
|
1605
|
+
extractMode: "crop-roi",
|
|
1606
|
+
inputClasses: null,
|
|
1607
|
+
outputClasses: ["mask"],
|
|
1608
|
+
models: [...SEGMENTATION_REFINER_MODELS],
|
|
1609
|
+
defaultModelId: "u2netp",
|
|
1610
|
+
enabledByDefault: false,
|
|
1611
|
+
defaultConfidence: 0,
|
|
1612
|
+
group: "Segmentation"
|
|
1613
|
+
});
|
|
1614
|
+
new PipelineStepBase({
|
|
1615
|
+
id: "instance-segmentation",
|
|
1616
|
+
name: "Instance Segmentation",
|
|
1617
|
+
slot: "refiner",
|
|
1618
|
+
postprocessor: "yolo-seg",
|
|
1619
|
+
extractMode: "full-frame",
|
|
1620
|
+
inputClasses: null,
|
|
1621
|
+
outputClasses: ["mask"],
|
|
1622
|
+
models: [...INSTANCE_SEGMENTATION_MODELS],
|
|
1623
|
+
defaultModelId: "yolo26n-seg",
|
|
1624
|
+
enabledByDefault: false,
|
|
1625
|
+
defaultConfidence: .25,
|
|
1626
|
+
labels: COCO_80_LABELS.map((l) => l.id),
|
|
1627
|
+
classMap: COCO_TO_MACRO,
|
|
1628
|
+
group: "Segmentation"
|
|
1629
|
+
});
|
|
1630
|
+
var STEP_OBJECT_DETECTION_INSTANCE = new ObjectDetectionStep();
|
|
1631
|
+
var STEP_AUDIO_CLASSIFIER_INSTANCE = new AudioClassifierStep();
|
|
1632
|
+
var ALL_PIPELINE_STEPS = [
|
|
1633
|
+
STEP_OBJECT_DETECTION_INSTANCE,
|
|
1634
|
+
STEP_FACE_DETECTION,
|
|
1635
|
+
STEP_FACE_EMBEDDING,
|
|
1636
|
+
STEP_CLIP_EMBEDDING,
|
|
1637
|
+
STEP_PLATE_DETECTION,
|
|
1638
|
+
STEP_PLATE_OCR,
|
|
1639
|
+
new AnimalClassifierStep(),
|
|
1640
|
+
STEP_BIRD_CLASSIFIER,
|
|
1641
|
+
STEP_VEHICLE_CLASSIFIER,
|
|
1642
|
+
STEP_SEGMENTATION,
|
|
1643
|
+
STEP_AUDIO_CLASSIFIER_INSTANCE
|
|
1644
|
+
];
|
|
1645
|
+
/** Compat: flat array of StepDefinition for existing consumers */
|
|
1646
|
+
var ALL_STEPS = ALL_PIPELINE_STEPS.map((s) => s.definition);
|
|
1647
|
+
var STEP_MAP = new Map(ALL_PIPELINE_STEPS.map((s) => [s.definition.id, s]));
|
|
1648
|
+
/**
|
|
1649
|
+
* Look up a pipeline step by ID (class instance with getConfigSchema).
|
|
1650
|
+
* @throws if the step ID is not registered.
|
|
1651
|
+
*/
|
|
1652
|
+
function getStep(stepId) {
|
|
1653
|
+
const step = STEP_MAP.get(stepId);
|
|
1654
|
+
if (!step) throw new Error(`Unknown pipeline step: "${stepId}". Available: ${ALL_PIPELINE_STEPS.map((s) => s.definition.id).join(", ")}`);
|
|
1655
|
+
return step;
|
|
1656
|
+
}
|
|
1657
|
+
/**
|
|
1658
|
+
* Look up a step definition by ID (compat shortcut).
|
|
1659
|
+
* @throws if the step ID is not registered.
|
|
1660
|
+
*/
|
|
1661
|
+
function getStepDefinition(stepId) {
|
|
1662
|
+
return getStep(stepId).definition;
|
|
1663
|
+
}
|
|
1664
|
+
/**
|
|
1665
|
+
* Get the default model ID for a step given the current model format.
|
|
1666
|
+
*
|
|
1667
|
+
* Resolution order:
|
|
1668
|
+
* 1. `def.defaultModelIdByFormat?.[format]` — the step's declared
|
|
1669
|
+
* per-format preference — if that model exists in `def.models` AND
|
|
1670
|
+
* has a build for `format`.
|
|
1671
|
+
* 2. `def.defaultModelId` — the step's plain declared default — if it
|
|
1672
|
+
* exists in `def.models` AND has a build for `format`.
|
|
1673
|
+
* 3. The smallest-by-size model among those with a `format` build
|
|
1674
|
+
* (legacy fallback, preserved for steps/formats with no declared
|
|
1675
|
+
* preference reachable).
|
|
1676
|
+
* 4. `def.defaultModelId` unchanged, when ZERO models have a `format`
|
|
1677
|
+
* build — an unloadable case flagged elsewhere, not resolved here.
|
|
1678
|
+
*/
|
|
1679
|
+
function getDefaultModelForFormat(stepId, format) {
|
|
1680
|
+
const def = getStepDefinition(stepId);
|
|
1681
|
+
const hasFormatBuild = (modelId) => def.models.find((m) => m.id === modelId)?.formats[format] !== void 0;
|
|
1682
|
+
const declaredForFormat = def.defaultModelIdByFormat?.[format];
|
|
1683
|
+
if (declaredForFormat !== void 0 && hasFormatBuild(declaredForFormat)) return declaredForFormat;
|
|
1684
|
+
if (hasFormatBuild(def.defaultModelId)) return def.defaultModelId;
|
|
1685
|
+
const available = def.models.filter((m) => m.formats[format] && m.legacy !== true);
|
|
1686
|
+
if (available.length === 0) return def.defaultModelId;
|
|
1687
|
+
return [...available].toSorted((a, b) => {
|
|
1688
|
+
return (a.formats[format]?.sizeMB ?? Infinity) - (b.formats[format]?.sizeMB ?? Infinity);
|
|
1689
|
+
})[0].id;
|
|
1690
|
+
}
|
|
1691
|
+
/**
|
|
1692
|
+
* The single node-local authority for turning a CHOSEN model (or none) into a
|
|
1693
|
+
* concrete model that has a build for this node's engine `format`. Fuses the
|
|
1694
|
+
* two previously-duplicated decisions (orchestrator `pickDefaultModelId` +
|
|
1695
|
+
* provider `substituteIncompatibleModels`):
|
|
1696
|
+
*
|
|
1697
|
+
* - chosen === null → the addon's default model for `format`.
|
|
1698
|
+
* - chosen is a catalog model → keep it if it has a `format` build; else
|
|
1699
|
+
* the addon's default model for `format`
|
|
1700
|
+
* (a substitution — the caller surfaces it).
|
|
1701
|
+
* - chosen is NOT in the catalog (custom/unknown) → returned unchanged; the
|
|
1702
|
+
* operator owns a custom model's format compat.
|
|
1703
|
+
* - unknown stepId → chosen returned unchanged (never throws).
|
|
1704
|
+
*/
|
|
1705
|
+
function resolveModelForFormat(stepId, chosenModelId, format) {
|
|
1706
|
+
let def;
|
|
1707
|
+
try {
|
|
1708
|
+
def = getStepDefinition(stepId);
|
|
1709
|
+
} catch {
|
|
1710
|
+
return chosenModelId ?? "";
|
|
1711
|
+
}
|
|
1712
|
+
if (chosenModelId === null) return getDefaultModelForFormat(stepId, format);
|
|
1713
|
+
const entry = def.models.find((m) => m.id === chosenModelId);
|
|
1714
|
+
if (!entry) return chosenModelId;
|
|
1715
|
+
if (entry.formats[format]) return chosenModelId;
|
|
1716
|
+
return getDefaultModelForFormat(stepId, format);
|
|
1717
|
+
}
|
|
1718
|
+
//#endregion
|
|
1719
|
+
//#region src/detection-pipeline/default-detection-model.ts
|
|
1720
|
+
/** The object-detection step id — the only slot this resolver applies to. */
|
|
1721
|
+
var OBJECT_DETECTION_STEP_ID = "object-detection";
|
|
1722
|
+
/**
|
|
1723
|
+
* Balanced default object-detection model per accelerator class (measured on
|
|
1724
|
+
* COCO val2017). `'cpu'` maps to `null` → the caller substitutes the step's
|
|
1725
|
+
* own `defaultModelId` (`yolo26n`), the safe nano that does not collapse on
|
|
1726
|
+
* small objects. The three accelerated ids are reduced-input YOLO26 variants
|
|
1727
|
+
* shipped in the catalog (onnx + coreml + openvino builds each).
|
|
1728
|
+
*/
|
|
1729
|
+
var MODEL_BY_CLASS = {
|
|
1730
|
+
"apple-ane": "yolo26l-256",
|
|
1731
|
+
"intel-npu": "yolo26m-320",
|
|
1732
|
+
"intel-gpu": "yolo26m-256",
|
|
1733
|
+
cpu: null
|
|
1734
|
+
};
|
|
1735
|
+
/**
|
|
1736
|
+
* Classify a node's engine into an {@link AcceleratorClass} from its resolved
|
|
1737
|
+
* `backend` + `device`. Pure, total, never throws.
|
|
1738
|
+
*
|
|
1739
|
+
* OpenVINO's default device is `'auto'` (the python pool resolves NPU > GPU >
|
|
1740
|
+
* CPU at load). Because OpenVINO is only ever OFFERED when an Intel iGPU is
|
|
1741
|
+
* present, the GPU is the guaranteed accelerator while the NPU is an explicit
|
|
1742
|
+
* opt-in — so `'auto'` (and any non-cpu/non-npu device) is treated as
|
|
1743
|
+
* GPU-class conservatively. An operator who explicitly pins `device: 'npu'`
|
|
1744
|
+
* gets the NPU-tuned m-320.
|
|
1745
|
+
*
|
|
1746
|
+
* Also accepts the accelerator carried in the `backend` slot (e.g. `'ane'`,
|
|
1747
|
+
* `'mps'`, `'npu'`, `'intel-gpu'`) so a caller passing a device-style backend
|
|
1748
|
+
* still classifies correctly — real callers pass onnx/openvino/coreml.
|
|
1749
|
+
*/
|
|
1750
|
+
function classifyAccelerator(backend, device) {
|
|
1751
|
+
const b = backend.toLowerCase();
|
|
1752
|
+
const d = device?.toLowerCase() ?? null;
|
|
1753
|
+
if (b === "coreml" || b === "ane" || b === "mps") return "apple-ane";
|
|
1754
|
+
if (b === "openvino") {
|
|
1755
|
+
if (d === "npu") return "intel-npu";
|
|
1756
|
+
if (d === "cpu") return "cpu";
|
|
1757
|
+
return "intel-gpu";
|
|
1758
|
+
}
|
|
1759
|
+
if (b === "npu" || b === "intel-npu") return "intel-npu";
|
|
1760
|
+
if (b === "gpu" || b === "intel-gpu" || b === "gpu-openvino") return "intel-gpu";
|
|
1761
|
+
return "cpu";
|
|
1762
|
+
}
|
|
1763
|
+
/**
|
|
1764
|
+
* Resolve the hardware-aware default object-detection model id for a node's
|
|
1765
|
+
* engine `(backend, device, format)`.
|
|
1766
|
+
*
|
|
1767
|
+
* Guarantees:
|
|
1768
|
+
* - never throws (hot default-tree path);
|
|
1769
|
+
* - never returns a model id that is not in the object-detection catalog
|
|
1770
|
+
* with a build for `format` — an unmapped backend, a `'cpu'` class, a
|
|
1771
|
+
* mapped id missing from the catalog, or a mapped id without a `format`
|
|
1772
|
+
* build all fall back to the step's own `defaultModelId` (`yolo26n`).
|
|
1773
|
+
*
|
|
1774
|
+
* `getStepDef` is injectable (defaults to the real catalog lookup) so the
|
|
1775
|
+
* missing-build fallback branch is unit-testable without a live catalog —
|
|
1776
|
+
* mirrors `collectZeroBuildIssues`' injection seam.
|
|
1777
|
+
*/
|
|
1778
|
+
function resolveDefaultDetectionModel(backend, device, format, getStepDef = getStepDefinition) {
|
|
1779
|
+
let def;
|
|
1780
|
+
try {
|
|
1781
|
+
def = getStepDef(OBJECT_DETECTION_STEP_ID);
|
|
1782
|
+
} catch {
|
|
1783
|
+
return "yolo26n";
|
|
1784
|
+
}
|
|
1785
|
+
const fallback = def.defaultModelId;
|
|
1786
|
+
const candidate = MODEL_BY_CLASS[classifyAccelerator(backend, device)];
|
|
1787
|
+
if (candidate === null) return fallback;
|
|
1788
|
+
const entry = def.models.find((m) => m.id === candidate);
|
|
1789
|
+
if (!entry || entry.formats[format] === void 0) return fallback;
|
|
1790
|
+
return candidate;
|
|
1791
|
+
}
|
|
1792
|
+
//#endregion
|
|
1793
|
+
//#region src/detection-pipeline/engine/shared-inference-pool.ts
|
|
1794
|
+
/**
|
|
1795
|
+
* SharedInferencePool — TypeScript wrapper for inference_pool.py.
|
|
1796
|
+
*
|
|
1797
|
+
* Multi-process orchestrator. Owns N `PoolWorker` instances, each
|
|
1798
|
+
* holding its own Python subprocess + MLModel copy, and round-robins
|
|
1799
|
+
* inference dispatches across them. Model management (load/unload/
|
|
1800
|
+
* replace) propagates to all workers so every subprocess holds the
|
|
1801
|
+
* same model set at the same indices.
|
|
1802
|
+
*
|
|
1803
|
+
* Wire format (per worker):
|
|
1804
|
+
* Request: [4B total_len][4B req_id][1B msg_type][payload]
|
|
1805
|
+
* Response: [4B total_len][4B req_id][JSON payload]
|
|
1806
|
+
*/
|
|
1807
|
+
var MSG_COMMAND = 0;
|
|
1808
|
+
var MSG_INFER_JPEG = 1;
|
|
1809
|
+
var MSG_INFER_RAW = 2;
|
|
1810
|
+
var MSG_CACHE_FRAME = 4;
|
|
1811
|
+
var MSG_INFER_CACHED = 5;
|
|
1812
|
+
/**
|
|
1813
|
+
* Cross-camera batching opcode — N items packed into a single IPC
|
|
1814
|
+
* frame. Wire shape:
|
|
1815
|
+
* [1B model_idx][1B count][N × (4B w | 4B h | 1B fmt | 4B size | raw bytes)]
|
|
1816
|
+
* Python pool dispatches each item via `dispatcher.run` (asyncio.gather)
|
|
1817
|
+
* and returns a JSON array of detections in the same order.
|
|
1818
|
+
*/
|
|
1819
|
+
var MSG_INFER_BATCH = 3;
|
|
1820
|
+
var PREFIX_LEN = 9;
|
|
1821
|
+
/**
|
|
1822
|
+
* Wire-level enum for the raw-frame fast path. Values are append-only:
|
|
1823
|
+
* the Python pool reads the byte directly off the IPC frame; reordering
|
|
1824
|
+
* existing entries breaks all in-flight workers.
|
|
1825
|
+
*/
|
|
1826
|
+
var RAW_FMT_CODE = {
|
|
1827
|
+
rgb: 0,
|
|
1828
|
+
bgr: 1,
|
|
1829
|
+
gray: 2
|
|
1830
|
+
};
|
|
1831
|
+
var PoolHandle = class {
|
|
1832
|
+
pool;
|
|
1833
|
+
modelIndex;
|
|
1834
|
+
runtime = "onnx";
|
|
1835
|
+
device = "cpu";
|
|
1836
|
+
constructor(pool, modelIndex) {
|
|
1837
|
+
this.pool = pool;
|
|
1838
|
+
this.modelIndex = modelIndex;
|
|
1839
|
+
}
|
|
1840
|
+
async infer(input) {
|
|
1841
|
+
const start = performance.now();
|
|
1842
|
+
return {
|
|
1843
|
+
structured: input.kind === "raw" ? await this.pool.inferRaw(this.modelIndex, input.data, input.width, input.height, input.format) : await this.pool.infer(this.modelIndex, input.data),
|
|
1844
|
+
inferenceMs: performance.now() - start
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
/**
|
|
1848
|
+
* Inference on a frame previously cached in the Python pool via
|
|
1849
|
+
* `pool.cacheFrame()`. Sends only 5 bytes through the pipe instead
|
|
1850
|
+
* of 1.2MB raw data — eliminates the pipe transfer bottleneck.
|
|
1851
|
+
*/
|
|
1852
|
+
async inferFromCache(frameId) {
|
|
1853
|
+
const start = performance.now();
|
|
1854
|
+
return {
|
|
1855
|
+
structured: await this.pool.inferCached(this.modelIndex, frameId),
|
|
1856
|
+
inferenceMs: performance.now() - start
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
async dispose() {}
|
|
1860
|
+
};
|
|
1861
|
+
/**
|
|
1862
|
+
* How long to wait for a worker to exit on SIGTERM before escalating to
|
|
1863
|
+
* SIGKILL. A worker idle between frames exits well under this; only a worker
|
|
1864
|
+
* stuck in a native inference call (openvino/onnx C++) needs the escalation.
|
|
1865
|
+
*/
|
|
1866
|
+
var POOL_WORKER_TERM_GRACE_MS = 2e3;
|
|
1867
|
+
/**
|
|
1868
|
+
* Terminate a spawned child GRACEFULLY THEN FORCEFULLY: end stdin, send
|
|
1869
|
+
* SIGTERM, and if the process hasn't exited within `graceMs`, send SIGKILL.
|
|
1870
|
+
* Resolves once the process has exited (or was already dead).
|
|
1871
|
+
*
|
|
1872
|
+
* The old teardown sent SIGTERM and dropped the reference immediately — a
|
|
1873
|
+
* worker busy in a native inference loop ignores SIGTERM and ORPHANS at full
|
|
1874
|
+
* CPU (observed after an engine re-spin). SIGKILL cannot be ignored, so the
|
|
1875
|
+
* escalation guarantees the process dies. Exported for tests.
|
|
1876
|
+
*/
|
|
1877
|
+
async function terminateChild(proc, graceMs) {
|
|
1878
|
+
if (proc.exitCode !== null || proc.signalCode !== null) return;
|
|
1879
|
+
try {
|
|
1880
|
+
proc.stdin?.end();
|
|
1881
|
+
} catch {}
|
|
1882
|
+
await new Promise((resolve) => {
|
|
1883
|
+
let settled = false;
|
|
1884
|
+
let timer = null;
|
|
1885
|
+
const done = () => {
|
|
1886
|
+
if (settled) return;
|
|
1887
|
+
settled = true;
|
|
1888
|
+
if (timer) clearTimeout(timer);
|
|
1889
|
+
resolve();
|
|
1890
|
+
};
|
|
1891
|
+
proc.once("exit", done);
|
|
1892
|
+
try {
|
|
1893
|
+
proc.kill("SIGTERM");
|
|
1894
|
+
} catch {
|
|
1895
|
+
done();
|
|
1896
|
+
return;
|
|
1420
1897
|
}
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1898
|
+
timer = setTimeout(() => {
|
|
1899
|
+
try {
|
|
1900
|
+
proc.kill("SIGKILL");
|
|
1901
|
+
} catch {}
|
|
1902
|
+
done();
|
|
1903
|
+
}, graceMs);
|
|
1904
|
+
});
|
|
1905
|
+
}
|
|
1906
|
+
var PoolWorker = class {
|
|
1907
|
+
process = null;
|
|
1908
|
+
receiveBuffer = Buffer.alloc(0);
|
|
1909
|
+
pending = /* @__PURE__ */ new Map();
|
|
1910
|
+
nextRequestId = 1;
|
|
1911
|
+
ready = false;
|
|
1912
|
+
log;
|
|
1913
|
+
opts;
|
|
1914
|
+
constructor(opts) {
|
|
1915
|
+
this.opts = opts;
|
|
1916
|
+
this.log = opts.logger;
|
|
1917
|
+
}
|
|
1918
|
+
getPid() {
|
|
1919
|
+
return this.process?.pid ?? null;
|
|
1920
|
+
}
|
|
1921
|
+
isReady() {
|
|
1922
|
+
return this.ready;
|
|
1923
|
+
}
|
|
1924
|
+
async initialize(initialModels) {
|
|
1925
|
+
this.process = spawn(this.opts.pythonPath, [this.opts.scriptPath], { stdio: [
|
|
1926
|
+
"pipe",
|
|
1927
|
+
"pipe",
|
|
1928
|
+
"pipe"
|
|
1929
|
+
] });
|
|
1930
|
+
if (!this.process.stdout || !this.process.stdin) throw new Error("PoolWorker: failed to create process pipes");
|
|
1931
|
+
this.process.stderr?.on("data", (chunk) => {
|
|
1932
|
+
for (const line of chunk.toString().split("\n")) {
|
|
1933
|
+
const trimmed = line.trim();
|
|
1934
|
+
if (trimmed) this.log.warn(`[${this.opts.workerLabel}] ${trimmed}`);
|
|
1935
|
+
}
|
|
1936
|
+
});
|
|
1937
|
+
this.process.on("error", (err) => {
|
|
1938
|
+
this.log.error("Worker process error", { meta: {
|
|
1939
|
+
worker: this.opts.workerLabel,
|
|
1940
|
+
error: err.message
|
|
1941
|
+
} });
|
|
1942
|
+
this.rejectAll(err);
|
|
1943
|
+
});
|
|
1944
|
+
this.process.on("exit", (code) => {
|
|
1945
|
+
if (code !== 0 && code !== null) {
|
|
1946
|
+
this.log.error("Worker process exited", { meta: {
|
|
1947
|
+
worker: this.opts.workerLabel,
|
|
1948
|
+
code
|
|
1949
|
+
} });
|
|
1950
|
+
this.rejectAll(/* @__PURE__ */ new Error(`Worker process exited with code ${code}`));
|
|
1951
|
+
}
|
|
1952
|
+
this.ready = false;
|
|
1953
|
+
});
|
|
1954
|
+
this.process.stdout.on("data", (chunk) => {
|
|
1955
|
+
this.receiveBuffer = Buffer.concat([this.receiveBuffer, chunk]);
|
|
1956
|
+
this.drainResponses();
|
|
1957
|
+
});
|
|
1958
|
+
const config = {
|
|
1959
|
+
runtime: this.opts.poolRuntime,
|
|
1960
|
+
concurrency: this.opts.concurrency,
|
|
1961
|
+
models: initialModels.map((m) => serializeModelConfig(m))
|
|
1962
|
+
};
|
|
1963
|
+
if (this.opts.tuning) {
|
|
1964
|
+
config["batch_mode"] = this.opts.tuning.batchMode;
|
|
1965
|
+
config["window_ms"] = this.opts.tuning.windowMs;
|
|
1966
|
+
config["max_batch_size"] = this.opts.tuning.maxBatchSize;
|
|
1967
|
+
config["num_streams"] = this.opts.tuning.numStreams;
|
|
1968
|
+
config["intra_op_threads"] = this.opts.tuning.intraOpThreads;
|
|
1446
1969
|
}
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1970
|
+
const configBuf = Buffer.from(JSON.stringify(config), "utf8");
|
|
1971
|
+
this.writeFrame(0, MSG_COMMAND, configBuf);
|
|
1972
|
+
return new Promise((resolve, reject) => {
|
|
1973
|
+
const timeout = setTimeout(() => {
|
|
1974
|
+
this.pending.delete(0);
|
|
1975
|
+
reject(/* @__PURE__ */ new Error(`PoolWorker[${this.opts.workerLabel}]: timeout waiting for ready (120s)`));
|
|
1976
|
+
}, 12e4);
|
|
1977
|
+
this.pending.set(0, {
|
|
1978
|
+
resolve: (result) => {
|
|
1979
|
+
clearTimeout(timeout);
|
|
1980
|
+
if (result["status"] === "ready") {
|
|
1981
|
+
this.ready = true;
|
|
1982
|
+
const loadedCount = result["models"];
|
|
1983
|
+
const startupMs = result["startupMs"];
|
|
1984
|
+
resolve({
|
|
1985
|
+
startupMs,
|
|
1986
|
+
loadedCount,
|
|
1987
|
+
workers: result["workers"] ?? 1
|
|
1988
|
+
});
|
|
1989
|
+
} else reject(/* @__PURE__ */ new Error(`Unexpected pool status: ${JSON.stringify(result)}`));
|
|
1990
|
+
},
|
|
1991
|
+
reject: (err) => {
|
|
1992
|
+
clearTimeout(timeout);
|
|
1993
|
+
reject(err);
|
|
1994
|
+
}
|
|
1995
|
+
});
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
async infer(modelByte, jpeg) {
|
|
1999
|
+
this.ensureReady();
|
|
2000
|
+
const payload = Buffer.concat([Buffer.from([modelByte]), jpeg]);
|
|
2001
|
+
return this.dispatch(MSG_INFER_JPEG, payload);
|
|
2002
|
+
}
|
|
2003
|
+
async inferRaw(modelByte, raw, width, height, format) {
|
|
2004
|
+
this.ensureReady();
|
|
2005
|
+
const fmtCode = RAW_FMT_CODE[format];
|
|
2006
|
+
const rawHeader = Buffer.allocUnsafe(10);
|
|
2007
|
+
rawHeader[0] = modelByte;
|
|
2008
|
+
rawHeader.writeUInt32LE(width, 1);
|
|
2009
|
+
rawHeader.writeUInt32LE(height, 5);
|
|
2010
|
+
rawHeader[9] = fmtCode;
|
|
2011
|
+
return this.dispatchMulti(MSG_INFER_RAW, rawHeader.length + raw.length, [rawHeader, raw]);
|
|
2012
|
+
}
|
|
2013
|
+
async inferBatch(modelByte, items) {
|
|
2014
|
+
this.ensureReady();
|
|
2015
|
+
if (items.length === 0) return [];
|
|
2016
|
+
const headerSize = 2 + items.length * 13;
|
|
2017
|
+
const totalRawSize = items.reduce((sum, it) => sum + it.raw.length, 0);
|
|
2018
|
+
const payload = Buffer.allocUnsafe(headerSize + totalRawSize);
|
|
2019
|
+
payload[0] = modelByte;
|
|
2020
|
+
payload[1] = items.length;
|
|
2021
|
+
let offset = 2;
|
|
2022
|
+
for (const item of items) {
|
|
2023
|
+
payload.writeUInt32LE(item.width, offset);
|
|
2024
|
+
payload.writeUInt32LE(item.height, offset + 4);
|
|
2025
|
+
payload[offset + 8] = RAW_FMT_CODE[item.format];
|
|
2026
|
+
payload.writeUInt32LE(item.raw.length, offset + 9);
|
|
2027
|
+
offset += 13;
|
|
2028
|
+
item.raw.copy(payload, offset);
|
|
2029
|
+
offset += item.raw.length;
|
|
1472
2030
|
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
2031
|
+
const response = await this.dispatch(MSG_INFER_BATCH, payload);
|
|
2032
|
+
const results = response["results"];
|
|
2033
|
+
if (!Array.isArray(results)) throw new Error(`PoolWorker.inferBatch: malformed response: ${JSON.stringify(response)}`);
|
|
2034
|
+
return results;
|
|
2035
|
+
}
|
|
2036
|
+
async cacheFrame(frameId, raw, width, height, format) {
|
|
2037
|
+
this.ensureReady();
|
|
2038
|
+
const fmtCode = RAW_FMT_CODE[format];
|
|
2039
|
+
const header = Buffer.allocUnsafe(13);
|
|
2040
|
+
header.writeUInt32LE(frameId, 0);
|
|
2041
|
+
header.writeUInt32LE(width, 4);
|
|
2042
|
+
header.writeUInt32LE(height, 8);
|
|
2043
|
+
header[12] = fmtCode;
|
|
2044
|
+
const payloadLen = header.length + raw.length;
|
|
2045
|
+
const result = await this.dispatchMulti(MSG_CACHE_FRAME, payloadLen, [header, raw]);
|
|
2046
|
+
if (result["error"]) throw new Error(String(result["error"]));
|
|
2047
|
+
}
|
|
2048
|
+
async inferCached(modelByte, frameId) {
|
|
2049
|
+
this.ensureReady();
|
|
2050
|
+
const payload = Buffer.allocUnsafe(5);
|
|
2051
|
+
payload[0] = modelByte;
|
|
2052
|
+
payload.writeUInt32LE(frameId, 1);
|
|
2053
|
+
return this.dispatch(MSG_INFER_CACHED, payload);
|
|
2054
|
+
}
|
|
2055
|
+
async sendCommand(cmd) {
|
|
2056
|
+
this.ensureReady();
|
|
2057
|
+
const payload = Buffer.from(JSON.stringify(cmd), "utf8");
|
|
2058
|
+
return await this.dispatch(MSG_COMMAND, payload);
|
|
2059
|
+
}
|
|
2060
|
+
async dispose() {
|
|
2061
|
+
const proc = this.process;
|
|
2062
|
+
if (!proc) return;
|
|
2063
|
+
this.process = null;
|
|
2064
|
+
this.ready = false;
|
|
2065
|
+
await terminateChild(proc, POOL_WORKER_TERM_GRACE_MS);
|
|
2066
|
+
}
|
|
2067
|
+
dispatch(msgType, payload) {
|
|
2068
|
+
const reqId = this.allocRequestId();
|
|
2069
|
+
return new Promise((resolve, reject) => {
|
|
2070
|
+
this.pending.set(reqId, {
|
|
2071
|
+
resolve,
|
|
2072
|
+
reject
|
|
2073
|
+
});
|
|
2074
|
+
try {
|
|
2075
|
+
this.writeFrame(reqId, msgType, payload);
|
|
2076
|
+
} catch (err) {
|
|
2077
|
+
this.pending.delete(reqId);
|
|
2078
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
2079
|
+
}
|
|
2080
|
+
});
|
|
2081
|
+
}
|
|
2082
|
+
dispatchMulti(msgType, payloadLen, parts) {
|
|
2083
|
+
const reqId = this.allocRequestId();
|
|
2084
|
+
return new Promise((resolve, reject) => {
|
|
2085
|
+
this.pending.set(reqId, {
|
|
2086
|
+
resolve,
|
|
2087
|
+
reject
|
|
2088
|
+
});
|
|
2089
|
+
try {
|
|
2090
|
+
if (!this.process?.stdin) throw new Error("PoolWorker: not initialized");
|
|
2091
|
+
const prefix = Buffer.allocUnsafe(PREFIX_LEN);
|
|
2092
|
+
prefix.writeUInt32LE(5 + payloadLen, 0);
|
|
2093
|
+
prefix.writeUInt32LE(reqId, 4);
|
|
2094
|
+
prefix[8] = msgType;
|
|
2095
|
+
this.process.stdin.write(prefix);
|
|
2096
|
+
for (const part of parts) this.process.stdin.write(part);
|
|
2097
|
+
} catch (err) {
|
|
2098
|
+
this.pending.delete(reqId);
|
|
2099
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
2100
|
+
}
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
allocRequestId() {
|
|
2104
|
+
let id = this.nextRequestId;
|
|
2105
|
+
this.nextRequestId = id >= 4294967295 ? 1 : id + 1;
|
|
2106
|
+
while (this.pending.has(id)) {
|
|
2107
|
+
id = this.nextRequestId;
|
|
2108
|
+
this.nextRequestId = id >= 4294967295 ? 1 : id + 1;
|
|
1498
2109
|
}
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
2110
|
+
return id;
|
|
2111
|
+
}
|
|
2112
|
+
writeFrame(reqId, msgType, payload) {
|
|
2113
|
+
if (!this.process?.stdin) throw new Error("PoolWorker: not initialized");
|
|
2114
|
+
const totalLen = 5 + payload.length;
|
|
2115
|
+
const header = Buffer.allocUnsafe(PREFIX_LEN);
|
|
2116
|
+
header.writeUInt32LE(totalLen, 0);
|
|
2117
|
+
header.writeUInt32LE(reqId, 4);
|
|
2118
|
+
header[8] = msgType;
|
|
2119
|
+
this.process.stdin.write(header);
|
|
2120
|
+
this.process.stdin.write(payload);
|
|
2121
|
+
}
|
|
2122
|
+
ensureReady() {
|
|
2123
|
+
if (!this.ready || !this.process?.stdin) throw new Error(`PoolWorker[${this.opts.workerLabel}]: not initialized`);
|
|
2124
|
+
}
|
|
2125
|
+
drainResponses() {
|
|
2126
|
+
while (this.receiveBuffer.length >= 8) {
|
|
2127
|
+
const totalLen = this.receiveBuffer.readUInt32LE(0);
|
|
2128
|
+
if (this.receiveBuffer.length < 4 + totalLen) break;
|
|
2129
|
+
const reqId = this.receiveBuffer.readUInt32LE(4);
|
|
2130
|
+
const jsonBytes = this.receiveBuffer.subarray(8, 4 + totalLen);
|
|
2131
|
+
this.receiveBuffer = this.receiveBuffer.subarray(4 + totalLen);
|
|
2132
|
+
const entry = this.pending.get(reqId);
|
|
2133
|
+
if (!entry) {
|
|
2134
|
+
this.log.warn("Response for unknown request id", { meta: {
|
|
2135
|
+
worker: this.opts.workerLabel,
|
|
2136
|
+
reqId
|
|
2137
|
+
} });
|
|
2138
|
+
continue;
|
|
2139
|
+
}
|
|
2140
|
+
this.pending.delete(reqId);
|
|
2141
|
+
try {
|
|
2142
|
+
const parsed = JSON.parse(jsonBytes.toString("utf8"));
|
|
2143
|
+
entry.resolve(parsed);
|
|
2144
|
+
} catch (err) {
|
|
2145
|
+
entry.reject(err instanceof Error ? err : new Error(String(err)));
|
|
2146
|
+
}
|
|
1524
2147
|
}
|
|
1525
|
-
},
|
|
1526
|
-
ovPrecisionVariant("yolov9t", "objectDetection/yolov9/openvino", "YOLOv9 Tiny", "fp16", 5, true),
|
|
1527
|
-
ovPrecisionVariant("yolov9t", "objectDetection/yolov9/openvino", "YOLOv9 Tiny", "int8", 3, true),
|
|
1528
|
-
ovPrecisionVariant("yolov9s", "objectDetection/yolov9/openvino", "YOLOv9 Small", "fp16", 15, true),
|
|
1529
|
-
ovPrecisionVariant("yolov9s", "objectDetection/yolov9/openvino", "YOLOv9 Small", "int8", 8, true),
|
|
1530
|
-
ovPrecisionVariant("yolo26n", "objectDetection/yolo26/openvino", "YOLO26 Nano", "fp16", 5, true),
|
|
1531
|
-
ovPrecisionVariant("yolo26n", "objectDetection/yolo26/openvino", "YOLO26 Nano", "int8", 3),
|
|
1532
|
-
ovPrecisionVariant("yolo26s", "objectDetection/yolo26/openvino", "YOLO26 Small", "fp16", 19, true),
|
|
1533
|
-
ovPrecisionVariant("yolo26s", "objectDetection/yolo26/openvino", "YOLO26 Small", "int8", 10),
|
|
1534
|
-
ovPrecisionVariant("yolo26m", "objectDetection/yolo26/openvino", "YOLO26 Medium", "fp16", 41, true),
|
|
1535
|
-
ovPrecisionVariant("yolo26m", "objectDetection/yolo26/openvino", "YOLO26 Medium", "int8", 21),
|
|
1536
|
-
ovPrecisionVariant("yolo26l", "objectDetection/yolo26/openvino", "YOLO26 Large", "fp16", 50, true),
|
|
1537
|
-
ovPrecisionVariant("yolo26l", "objectDetection/yolo26/openvino", "YOLO26 Large", "int8", 25),
|
|
1538
|
-
ovPrecisionVariant("yolo26x", "objectDetection/yolo26/openvino", "YOLO26 XLarge", "fp16", 112, true),
|
|
1539
|
-
ovPrecisionVariant("yolo26x", "objectDetection/yolo26/openvino", "YOLO26 XLarge", "int8", 56, true)
|
|
1540
|
-
];
|
|
1541
|
-
var FACE_DETECTION_MODELS = [{
|
|
1542
|
-
id: "scrfd-2.5g",
|
|
1543
|
-
name: "SCRFD 2.5G",
|
|
1544
|
-
description: "SCRFD 2.5G — balanced face detection model",
|
|
1545
|
-
inputSize: {
|
|
1546
|
-
width: 640,
|
|
1547
|
-
height: 640
|
|
1548
|
-
},
|
|
1549
|
-
labels: [{
|
|
1550
|
-
id: "face",
|
|
1551
|
-
name: "Face"
|
|
1552
|
-
}],
|
|
1553
|
-
preprocessMode: "letterbox",
|
|
1554
|
-
formats: {
|
|
1555
|
-
onnx: {
|
|
1556
|
-
url: hf("faceDetection/scrfd/onnx/camstack-scrfd-2.5g.onnx"),
|
|
1557
|
-
sizeMB: 3.1
|
|
1558
|
-
},
|
|
1559
|
-
coreml: {
|
|
1560
|
-
url: hf("faceDetection/scrfd/coreml/camstack-scrfd-2.5g.mlpackage"),
|
|
1561
|
-
sizeMB: 1.7,
|
|
1562
|
-
isDirectory: true,
|
|
1563
|
-
files: [...MLPACKAGE_FILES],
|
|
1564
|
-
runtimes: ["python"]
|
|
1565
|
-
},
|
|
1566
|
-
openvino: ovFormat(hf("faceDetection/scrfd/openvino/camstack-scrfd-2.5g.xml"), 1.8)
|
|
1567
2148
|
}
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
2149
|
+
rejectAll(err) {
|
|
2150
|
+
const entries = [...this.pending.values()];
|
|
2151
|
+
this.pending.clear();
|
|
2152
|
+
for (const entry of entries) entry.reject(err);
|
|
2153
|
+
}
|
|
2154
|
+
};
|
|
2155
|
+
var SharedInferencePool = class {
|
|
2156
|
+
pythonPath;
|
|
2157
|
+
poolRuntime;
|
|
2158
|
+
workers = [];
|
|
2159
|
+
nextDispatchIdx = 0;
|
|
2160
|
+
nextFreeIndex = 0;
|
|
2161
|
+
nextFrameId = 1;
|
|
2162
|
+
/**
|
|
2163
|
+
* Cumulative count of frames the Python pool SHED under overload
|
|
2164
|
+
* (`"dropped": true` responses from the per-model in-flight bound in
|
|
2165
|
+
* inference_pool.py). Without this the shed response is
|
|
2166
|
+
* indistinguishable from a genuine "no detections" result.
|
|
2167
|
+
*/
|
|
2168
|
+
droppedResponseCount = 0;
|
|
2169
|
+
log;
|
|
2170
|
+
concurrency;
|
|
2171
|
+
tuning;
|
|
2172
|
+
numWorkers;
|
|
2173
|
+
constructor(pythonPath, poolRuntime, logger, options = {}) {
|
|
2174
|
+
this.pythonPath = pythonPath;
|
|
2175
|
+
this.poolRuntime = poolRuntime;
|
|
2176
|
+
this.log = logger;
|
|
2177
|
+
this.concurrency = Math.max(1, options.concurrency ?? 1);
|
|
2178
|
+
this.tuning = options.tuning ?? null;
|
|
2179
|
+
this.numWorkers = Math.max(1, options.numWorkers ?? 1);
|
|
2180
|
+
}
|
|
2181
|
+
/** Pid of the first worker (for legacy callers). Use `getPids()` for all. */
|
|
2182
|
+
getPid() {
|
|
2183
|
+
return this.workers[0]?.getPid() ?? null;
|
|
2184
|
+
}
|
|
2185
|
+
getPids() {
|
|
2186
|
+
return this.workers.map((w) => w.getPid());
|
|
2187
|
+
}
|
|
2188
|
+
/**
|
|
2189
|
+
* Start N pool workers in parallel. Each worker loads the same
|
|
2190
|
+
* initial model set so subsequent inference calls land on a worker
|
|
2191
|
+
* with the requested model already in its slot map.
|
|
2192
|
+
*/
|
|
2193
|
+
async initialize(initialModels = []) {
|
|
2194
|
+
const scriptPath = this.resolveScriptPath();
|
|
2195
|
+
for (let i = 0; i < this.numWorkers; i++) this.workers.push(new PoolWorker({
|
|
2196
|
+
pythonPath: this.pythonPath,
|
|
2197
|
+
scriptPath,
|
|
2198
|
+
poolRuntime: this.poolRuntime,
|
|
2199
|
+
concurrency: this.concurrency,
|
|
2200
|
+
tuning: this.tuning,
|
|
2201
|
+
logger: this.log,
|
|
2202
|
+
workerLabel: `w${i}`
|
|
2203
|
+
}));
|
|
2204
|
+
const t0 = performance.now();
|
|
2205
|
+
const results = await Promise.all(this.workers.map((w) => w.initialize(initialModels)));
|
|
2206
|
+
const startupMs = Math.round(performance.now() - t0);
|
|
2207
|
+
this.nextFreeIndex = initialModels.length;
|
|
2208
|
+
const loadedCount = results[0]?.loadedCount ?? 0;
|
|
2209
|
+
const innerWorkers = results.reduce((s, r) => s + r.workers, 0);
|
|
2210
|
+
this.log.info("Pool ready", { meta: {
|
|
2211
|
+
loadedCount,
|
|
2212
|
+
startupMs,
|
|
2213
|
+
runtime: this.poolRuntime,
|
|
2214
|
+
workers: this.numWorkers,
|
|
2215
|
+
pythonWorkers: innerWorkers,
|
|
2216
|
+
pids: this.getPids()
|
|
2217
|
+
} });
|
|
2218
|
+
return {
|
|
2219
|
+
startupMs,
|
|
2220
|
+
loadedCount,
|
|
2221
|
+
workers: this.numWorkers
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
2224
|
+
async infer(modelIndex, jpeg) {
|
|
2225
|
+
const w = this.pickWorker();
|
|
2226
|
+
return this.trackDroppedResponse(await w.infer(this.encodeModelByte(modelIndex), jpeg), modelIndex);
|
|
2227
|
+
}
|
|
2228
|
+
async inferRaw(modelIndex, raw, width, height, format) {
|
|
2229
|
+
const w = this.pickWorker();
|
|
2230
|
+
return this.trackDroppedResponse(await w.inferRaw(this.encodeModelByte(modelIndex), raw, width, height, format), modelIndex);
|
|
1594
2231
|
}
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
name: "ArcFace R100",
|
|
1599
|
-
description: "ArcFace ResNet-100 — high-accuracy face recognition embeddings (512-d)",
|
|
1600
|
-
inputSize: {
|
|
1601
|
-
width: 112,
|
|
1602
|
-
height: 112
|
|
1603
|
-
},
|
|
1604
|
-
inputLayout: "nhwc",
|
|
1605
|
-
labels: [{
|
|
1606
|
-
id: "embedding",
|
|
1607
|
-
name: "Face Embedding"
|
|
1608
|
-
}],
|
|
1609
|
-
preprocessMode: "resize",
|
|
1610
|
-
faceAlignment: true,
|
|
1611
|
-
formats: {
|
|
1612
|
-
onnx: {
|
|
1613
|
-
url: hf("faceRecognition/arcface/onnx/camstack-arcface-arcface.onnx"),
|
|
1614
|
-
sizeMB: 130
|
|
1615
|
-
},
|
|
1616
|
-
coreml: {
|
|
1617
|
-
url: hf("faceRecognition/arcface/coreml/camstack-arcface-r100.mlpackage"),
|
|
1618
|
-
sizeMB: 65,
|
|
1619
|
-
isDirectory: true,
|
|
1620
|
-
files: [...MLPACKAGE_FILES],
|
|
1621
|
-
runtimes: ["python"]
|
|
1622
|
-
},
|
|
1623
|
-
openvino: ovFormat(hf("faceRecognition/arcface/openvino/camstack-arcface-r100.xml"), 65)
|
|
2232
|
+
async inferBatch(modelIndex, items) {
|
|
2233
|
+
if (items.length > 255) throw new Error(`SharedInferencePool.inferBatch: max 255 items per call, got ${items.length}`);
|
|
2234
|
+
return this.pickWorker().inferBatch(this.encodeModelByte(modelIndex), items);
|
|
1624
2235
|
}
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
description: "FaceNet-style face recognition embeddings (512-d) — hosted on plugin-models HF repo",
|
|
1629
|
-
inputSize: {
|
|
1630
|
-
width: 160,
|
|
1631
|
-
height: 160
|
|
1632
|
-
},
|
|
1633
|
-
labels: [{
|
|
1634
|
-
id: "embedding",
|
|
1635
|
-
name: "Face Embedding"
|
|
1636
|
-
}],
|
|
1637
|
-
preprocessMode: "resize",
|
|
1638
|
-
formats: {
|
|
1639
|
-
onnx: {
|
|
1640
|
-
url: hfScrypted("onnx/inception_resnet_v1/inception_resnet_v1.onnx"),
|
|
1641
|
-
sizeMB: 89.6
|
|
1642
|
-
},
|
|
1643
|
-
coreml: {
|
|
1644
|
-
url: hfScrypted("coreml/inception_resnet_v1/inception_resnet_v1.mlpackage"),
|
|
1645
|
-
sizeMB: 45,
|
|
1646
|
-
isDirectory: true,
|
|
1647
|
-
files: [...MLPACKAGE_FILES],
|
|
1648
|
-
runtimes: ["python"]
|
|
1649
|
-
},
|
|
1650
|
-
openvino: ovFormat(hf("faceRecognition/inception-resnet-v1/openvino/camstack-inception-resnet-v1.xml"), 45)
|
|
2236
|
+
async inferCached(modelIndex, frameId) {
|
|
2237
|
+
const w = this.pickWorker();
|
|
2238
|
+
return this.trackDroppedResponse(await w.inferCached(this.encodeModelByte(modelIndex), frameId), modelIndex);
|
|
1651
2239
|
}
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
height: 640
|
|
1660
|
-
},
|
|
1661
|
-
labels: [{
|
|
1662
|
-
id: "plate",
|
|
1663
|
-
name: "License Plate"
|
|
1664
|
-
}],
|
|
1665
|
-
preprocessMode: "letterbox",
|
|
1666
|
-
formats: {
|
|
1667
|
-
onnx: {
|
|
1668
|
-
url: hf("plateDetection/yolov8-plate/onnx/camstack-yolov8n-plate.onnx"),
|
|
1669
|
-
sizeMB: 12
|
|
1670
|
-
},
|
|
1671
|
-
coreml: {
|
|
1672
|
-
url: hf("plateDetection/yolov8-plate/coreml/camstack-yolov8n-plate.mlpackage"),
|
|
1673
|
-
sizeMB: 5.9,
|
|
1674
|
-
isDirectory: true,
|
|
1675
|
-
files: [...MLPACKAGE_FILES],
|
|
1676
|
-
runtimes: ["python"]
|
|
1677
|
-
},
|
|
1678
|
-
openvino: ovFormat(hf("plateDetection/yolov8-plate/openvino/camstack-yolov8n-plate.xml"), 6.1)
|
|
2240
|
+
/**
|
|
2241
|
+
* Total `"dropped": true` responses seen from the Python pool — frames
|
|
2242
|
+
* shed by the per-model in-flight bound under overload. Diagnostic
|
|
2243
|
+
* counter (monotonic for the pool's lifetime).
|
|
2244
|
+
*/
|
|
2245
|
+
getDroppedResponseCount() {
|
|
2246
|
+
return this.droppedResponseCount;
|
|
1679
2247
|
}
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
id: "vgg-english-g2",
|
|
1683
|
-
name: "VGG English G2",
|
|
1684
|
-
description: "EasyOCR VGG English G2 — text recognition for license plates and scene text",
|
|
1685
|
-
inputSize: {
|
|
1686
|
-
width: 384,
|
|
1687
|
-
height: 64
|
|
1688
|
-
},
|
|
1689
|
-
labels: [{
|
|
1690
|
-
id: "text",
|
|
1691
|
-
name: "Plate Text"
|
|
1692
|
-
}],
|
|
1693
|
-
preprocessMode: "resize",
|
|
1694
|
-
formats: {
|
|
1695
|
-
onnx: {
|
|
1696
|
-
url: hfScrypted("onnx/vgg_english_g2/vgg_english_g2.onnx"),
|
|
1697
|
-
sizeMB: 14.4
|
|
1698
|
-
},
|
|
1699
|
-
coreml: {
|
|
1700
|
-
url: hfScrypted("coreml/vgg_english_g2/vgg_english_g2.mlpackage"),
|
|
1701
|
-
sizeMB: 11.2,
|
|
1702
|
-
isDirectory: true,
|
|
1703
|
-
files: [...MLPACKAGE_FILES],
|
|
1704
|
-
runtimes: ["python"]
|
|
1705
|
-
},
|
|
1706
|
-
openvino: ovFormat(hf("plateRecognition/vgg_english_g2/openvino/vgg_english_g2.xml"), 7.2)
|
|
2248
|
+
getHandle(modelIndex) {
|
|
2249
|
+
return new PoolHandle(this, modelIndex);
|
|
1707
2250
|
}
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
2251
|
+
async cacheFrame(raw, width, height, format) {
|
|
2252
|
+
const frameId = this.nextFrameId++;
|
|
2253
|
+
await Promise.all(this.workers.map((w) => w.cacheFrame(frameId, raw, width, height, format)));
|
|
2254
|
+
return {
|
|
2255
|
+
frameId,
|
|
2256
|
+
width,
|
|
2257
|
+
height
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
async uncacheFrame(frameId) {
|
|
2261
|
+
await Promise.all(this.workers.map((w) => w.sendCommand({
|
|
2262
|
+
cmd: "uncache_frame",
|
|
2263
|
+
frameId
|
|
2264
|
+
})));
|
|
2265
|
+
}
|
|
2266
|
+
async loadModel(index, config) {
|
|
2267
|
+
const responses = await Promise.all(this.workers.map((w) => w.sendCommand({
|
|
2268
|
+
cmd: "load",
|
|
2269
|
+
index,
|
|
2270
|
+
config: serializeModelConfig(config)
|
|
2271
|
+
})));
|
|
2272
|
+
for (const resp of responses) if (resp.status !== "ok") throw new Error(`Failed to load model at index ${index}: ${resp.error ?? "unknown"}`);
|
|
2273
|
+
if (index >= this.nextFreeIndex) this.nextFreeIndex = index + 1;
|
|
2274
|
+
return { loadMs: Math.max(...responses.map((r) => r.loadMs ?? 0)) };
|
|
2275
|
+
}
|
|
2276
|
+
async unloadModel(index) {
|
|
2277
|
+
const responses = await Promise.all(this.workers.map((w) => w.sendCommand({
|
|
2278
|
+
cmd: "unload",
|
|
2279
|
+
index
|
|
2280
|
+
})));
|
|
2281
|
+
for (const resp of responses) if (resp.status !== "ok") throw new Error(`Failed to unload model at index ${index}: ${resp.error ?? "unknown"}`);
|
|
2282
|
+
}
|
|
2283
|
+
async replaceModel(index, config) {
|
|
2284
|
+
const responses = await Promise.all(this.workers.map((w) => w.sendCommand({
|
|
2285
|
+
cmd: "replace",
|
|
2286
|
+
index,
|
|
2287
|
+
config: serializeModelConfig(config)
|
|
2288
|
+
})));
|
|
2289
|
+
for (const resp of responses) if (resp.status !== "ok") throw new Error(`Failed to replace model at index ${index}: ${resp.error ?? "unknown"}`);
|
|
2290
|
+
return { loadMs: Math.max(...responses.map((r) => r.loadMs ?? 0)) };
|
|
2291
|
+
}
|
|
2292
|
+
async getStatus() {
|
|
2293
|
+
if (this.workers.length === 0) return [];
|
|
2294
|
+
return (await this.workers[0].sendCommand({ cmd: "status" })).models ?? [];
|
|
2295
|
+
}
|
|
2296
|
+
allocateIndex() {
|
|
2297
|
+
return this.nextFreeIndex++;
|
|
2298
|
+
}
|
|
2299
|
+
isReady() {
|
|
2300
|
+
return this.workers.length > 0 && this.workers.every((w) => w.isReady());
|
|
2301
|
+
}
|
|
2302
|
+
async dispose() {
|
|
2303
|
+
await Promise.all(this.workers.map((w) => w.dispose()));
|
|
2304
|
+
this.workers.length = 0;
|
|
2305
|
+
this.log.info("Pool process(es) terminated");
|
|
2306
|
+
}
|
|
2307
|
+
/**
|
|
2308
|
+
* Recognise a Python overload-shed response (`"dropped": true`) on the
|
|
2309
|
+
* single-frame inference paths. Previously indistinguishable from a
|
|
2310
|
+
* genuine empty detection result — now counted + debug-logged so
|
|
2311
|
+
* overload is measurable. The response passes through unchanged.
|
|
2312
|
+
*/
|
|
2313
|
+
trackDroppedResponse(result, modelIndex) {
|
|
2314
|
+
if (result["dropped"] === true) {
|
|
2315
|
+
this.droppedResponseCount++;
|
|
2316
|
+
this.log.debug("Python pool shed frame under overload", { meta: {
|
|
2317
|
+
modelIndex,
|
|
2318
|
+
droppedTotal: this.droppedResponseCount
|
|
2319
|
+
} });
|
|
2320
|
+
}
|
|
2321
|
+
return result;
|
|
2322
|
+
}
|
|
2323
|
+
pickWorker() {
|
|
2324
|
+
if (this.workers.length === 0) throw new Error("SharedInferencePool: not initialized");
|
|
2325
|
+
const idx = this.nextDispatchIdx;
|
|
2326
|
+
this.nextDispatchIdx = (this.nextDispatchIdx + 1) % this.workers.length;
|
|
2327
|
+
return this.workers[idx];
|
|
1736
2328
|
}
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
name: "NABirds (404 species)",
|
|
1741
|
-
description: "ResNet50 trained on NABirds — 404 North American species",
|
|
1742
|
-
inputSize: {
|
|
1743
|
-
width: 224,
|
|
1744
|
-
height: 224
|
|
1745
|
-
},
|
|
1746
|
-
inputNormalization: "imagenet",
|
|
1747
|
-
labels: [{
|
|
1748
|
-
id: "species",
|
|
1749
|
-
name: "Bird Species"
|
|
1750
|
-
}],
|
|
1751
|
-
preprocessMode: "resize",
|
|
1752
|
-
formats: {
|
|
1753
|
-
onnx: {
|
|
1754
|
-
url: hf("animalClassification/bird-nabirds/onnx/camstack-bird-nabirds-404.onnx"),
|
|
1755
|
-
sizeMB: 93
|
|
1756
|
-
},
|
|
1757
|
-
coreml: {
|
|
1758
|
-
url: hf("animalClassification/bird-nabirds/coreml/camstack-bird-nabirds-404.mlpackage"),
|
|
1759
|
-
sizeMB: 47,
|
|
1760
|
-
isDirectory: true,
|
|
1761
|
-
files: [...MLPACKAGE_FILES],
|
|
1762
|
-
runtimes: ["python"]
|
|
1763
|
-
},
|
|
1764
|
-
openvino: ovFormat(hf("animalClassification/bird-nabirds/openvino/camstack-bird-nabirds-404.xml"), 47)
|
|
1765
|
-
},
|
|
1766
|
-
extraFiles: [{
|
|
1767
|
-
url: hf("animalClassification/bird-nabirds/onnx/camstack-bird-nabirds-404-labels.json"),
|
|
1768
|
-
filename: "camstack-bird-nabirds-404-labels.json",
|
|
1769
|
-
sizeMB: .02
|
|
1770
|
-
}]
|
|
1771
|
-
}];
|
|
1772
|
-
var VEHICLE_CLASSIFIER_MODELS = [{
|
|
1773
|
-
id: "vehicle-type-efficientnet",
|
|
1774
|
-
name: "Vehicle Type (EfficientNet)",
|
|
1775
|
-
description: "EfficientNet-B4 vehicle make/model/year classifier — 8,949 classes from VMMRdb",
|
|
1776
|
-
inputSize: {
|
|
1777
|
-
width: 380,
|
|
1778
|
-
height: 380
|
|
1779
|
-
},
|
|
1780
|
-
inputNormalization: "imagenet",
|
|
1781
|
-
labels: [{
|
|
1782
|
-
id: "vehicle-type",
|
|
1783
|
-
name: "Vehicle Type"
|
|
1784
|
-
}],
|
|
1785
|
-
preprocessMode: "resize",
|
|
1786
|
-
formats: {
|
|
1787
|
-
onnx: {
|
|
1788
|
-
url: hf("vehicleClassification/efficientnet/onnx/camstack-vehicle-type-efficientnet.onnx"),
|
|
1789
|
-
sizeMB: 135
|
|
1790
|
-
},
|
|
1791
|
-
coreml: {
|
|
1792
|
-
url: hf("vehicleClassification/efficientnet/coreml/camstack-vehicle-type-efficientnet.mlpackage"),
|
|
1793
|
-
sizeMB: 10,
|
|
1794
|
-
isDirectory: true,
|
|
1795
|
-
files: [...MLPACKAGE_FILES],
|
|
1796
|
-
runtimes: ["python"]
|
|
1797
|
-
},
|
|
1798
|
-
openvino: ovFormat(hf("vehicleClassification/efficientnet/openvino/camstack-vehicle-type-efficientnet.xml"), 68)
|
|
1799
|
-
},
|
|
1800
|
-
extraFiles: [{
|
|
1801
|
-
url: hf("vehicleClassification/efficientnet/camstack-vehicle-type-labels.json"),
|
|
1802
|
-
filename: "camstack-vehicle-type-labels.json",
|
|
1803
|
-
sizeMB: .2
|
|
1804
|
-
}]
|
|
1805
|
-
}];
|
|
1806
|
-
var SEGMENTATION_REFINER_MODELS = [{
|
|
1807
|
-
id: "u2netp",
|
|
1808
|
-
name: "U2-Net Portable",
|
|
1809
|
-
description: "U2-Net-P — ultra-lightweight salient object segmentation (4.7 MB)",
|
|
1810
|
-
inputSize: {
|
|
1811
|
-
width: 320,
|
|
1812
|
-
height: 320
|
|
1813
|
-
},
|
|
1814
|
-
labels: [{
|
|
1815
|
-
id: "mask",
|
|
1816
|
-
name: "Segmentation Mask"
|
|
1817
|
-
}],
|
|
1818
|
-
preprocessMode: "resize",
|
|
1819
|
-
formats: {
|
|
1820
|
-
onnx: {
|
|
1821
|
-
url: hf("segmentationRefiner/u2netp/onnx/camstack-u2netp.onnx"),
|
|
1822
|
-
sizeMB: 5
|
|
1823
|
-
},
|
|
1824
|
-
coreml: {
|
|
1825
|
-
url: hf("segmentationRefiner/u2netp/coreml/camstack-u2netp.mlpackage"),
|
|
1826
|
-
sizeMB: 3,
|
|
1827
|
-
isDirectory: true,
|
|
1828
|
-
files: [...MLPACKAGE_FILES],
|
|
1829
|
-
runtimes: ["python"]
|
|
1830
|
-
},
|
|
1831
|
-
openvino: ovFormat(hf("segmentationRefiner/u2netp/openvino/camstack-u2netp.xml"), 2.5)
|
|
2329
|
+
encodeModelByte(modelIndex) {
|
|
2330
|
+
if (modelIndex < 0 || modelIndex > 254) throw new Error(`Model index ${modelIndex} out of range (0-254)`);
|
|
2331
|
+
return modelIndex;
|
|
1832
2332
|
}
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
}
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
2333
|
+
resolveScriptPath() {
|
|
2334
|
+
const candidates = [
|
|
2335
|
+
path$1.join(__dirname, "../../python/inference_pool.py"),
|
|
2336
|
+
path$1.join(__dirname, "../python/inference_pool.py"),
|
|
2337
|
+
path$1.join(__dirname, "../../../python/inference_pool.py")
|
|
2338
|
+
];
|
|
2339
|
+
try {
|
|
2340
|
+
const pkgPath = __require.resolve("@camstack/addon-detection-pipeline/package.json");
|
|
2341
|
+
candidates.unshift(path$1.join(path$1.dirname(pkgPath), "python", "inference_pool.py"));
|
|
2342
|
+
} catch {}
|
|
2343
|
+
const found = candidates.find((p) => fs.existsSync(p));
|
|
2344
|
+
if (!found) throw new Error(`inference_pool.py not found. Searched:\n${candidates.join("\n")}`);
|
|
2345
|
+
return found;
|
|
2346
|
+
}
|
|
2347
|
+
};
|
|
2348
|
+
function serializeModelConfig(config) {
|
|
2349
|
+
const result = {
|
|
2350
|
+
path: config.path,
|
|
2351
|
+
inputSize: config.inputSize,
|
|
2352
|
+
preprocessMode: config.preprocessMode,
|
|
2353
|
+
postprocessor: config.postprocessor,
|
|
2354
|
+
confidence: config.confidence
|
|
2355
|
+
};
|
|
2356
|
+
if (config.labels) result["labels"] = config.labels;
|
|
2357
|
+
if (config.charset) result["charset"] = config.charset;
|
|
2358
|
+
if (config.numClasses) result["numClasses"] = config.numClasses;
|
|
2359
|
+
if (config.strides) result["strides"] = config.strides;
|
|
2360
|
+
if (config.maskThreshold !== void 0) result["maskThreshold"] = config.maskThreshold;
|
|
2361
|
+
if (config.device) result["device"] = config.device;
|
|
2362
|
+
return result;
|
|
2363
|
+
}
|
|
2364
|
+
//#endregion
|
|
2365
|
+
//#region src/detection-pipeline/engine/step-utils.ts
|
|
2366
|
+
/**
|
|
2367
|
+
* Flatten a step tree into a list of enabled video steps (DFS).
|
|
2368
|
+
* Audio-classifier steps are excluded — they run independently
|
|
2369
|
+
* via the audio pipeline, not the video inference pool.
|
|
2370
|
+
*/
|
|
2371
|
+
function flattenEnabledVideoSteps(steps) {
|
|
2372
|
+
const result = [];
|
|
2373
|
+
const walk = (nodes) => {
|
|
2374
|
+
for (const step of nodes) {
|
|
2375
|
+
if (!step.enabled) continue;
|
|
2376
|
+
if (step.slot === "audio-classifier") continue;
|
|
2377
|
+
result.push(step);
|
|
2378
|
+
if (step.children?.length) walk(step.children);
|
|
1858
2379
|
}
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
2380
|
+
};
|
|
2381
|
+
walk(steps);
|
|
2382
|
+
return result;
|
|
2383
|
+
}
|
|
2384
|
+
//#endregion
|
|
2385
|
+
//#region src/detection-pipeline/engine/pipeline-model-manager.ts
|
|
2386
|
+
var PipelineModelManager = class {
|
|
2387
|
+
pool;
|
|
2388
|
+
buildPoolConfig;
|
|
2389
|
+
/** stepId → modelId → loaded entry. */
|
|
2390
|
+
loaded = /* @__PURE__ */ new Map();
|
|
2391
|
+
/** stepId → currently-active modelId (the runtime choice). */
|
|
2392
|
+
activeByStep = /* @__PURE__ */ new Map();
|
|
2393
|
+
/** Monotonic counter feeding the LRU tick on every touch. */
|
|
2394
|
+
lruClock = 0;
|
|
2395
|
+
log;
|
|
2396
|
+
maxModelsPerStep;
|
|
2397
|
+
constructor(pool, buildPoolConfig, logger, options) {
|
|
2398
|
+
this.pool = pool;
|
|
2399
|
+
this.buildPoolConfig = buildPoolConfig;
|
|
2400
|
+
this.log = logger;
|
|
2401
|
+
this.maxModelsPerStep = options?.maxModelsPerStep ?? 4;
|
|
2402
|
+
}
|
|
2403
|
+
/**
|
|
2404
|
+
* Apply a new pipeline configuration — driven by the runtime config
|
|
2405
|
+
* (camera-serving pipeline). Rewrites the *active* model per step,
|
|
2406
|
+
* keeping prior actives warm in extra slots up to `maxModelsPerStep`.
|
|
2407
|
+
*
|
|
2408
|
+
* Steps that disappear from `newSteps` get fully unloaded (every
|
|
2409
|
+
* model variant for that stepId).
|
|
2410
|
+
*/
|
|
2411
|
+
async applyConfig(newSteps) {
|
|
2412
|
+
const enabledSteps = flattenEnabledVideoSteps(newSteps);
|
|
2413
|
+
const diff = this.computeDiff(enabledSteps);
|
|
2414
|
+
for (const removed of diff.removed) {
|
|
2415
|
+
await this.unloadEntry(removed);
|
|
2416
|
+
this.log.info("Unloading step", { meta: {
|
|
2417
|
+
step: removed.stepId,
|
|
2418
|
+
modelId: removed.modelId,
|
|
2419
|
+
poolIndex: removed.poolIndex
|
|
2420
|
+
} });
|
|
2421
|
+
}
|
|
2422
|
+
for (const added of diff.added) {
|
|
2423
|
+
await this.ensureLoaded(added.addonId, added.modelId);
|
|
2424
|
+
this.activeByStep.set(added.addonId, added.modelId);
|
|
2425
|
+
this.log.info("Step loaded", { meta: {
|
|
2426
|
+
step: added.addonId,
|
|
2427
|
+
modelId: added.modelId,
|
|
2428
|
+
active: true
|
|
2429
|
+
} });
|
|
2430
|
+
}
|
|
2431
|
+
for (const { step, oldModelId } of diff.modelChanged) {
|
|
2432
|
+
await this.ensureLoaded(step.addonId, step.modelId);
|
|
2433
|
+
this.activeByStep.set(step.addonId, step.modelId);
|
|
2434
|
+
this.log.info("Active model switched", { meta: {
|
|
2435
|
+
step: step.addonId,
|
|
2436
|
+
fromModelId: oldModelId,
|
|
2437
|
+
toModelId: step.modelId
|
|
2438
|
+
} });
|
|
2439
|
+
await this.enforceLruCap(step.addonId);
|
|
1883
2440
|
}
|
|
1884
|
-
}
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
2441
|
+
}
|
|
2442
|
+
/**
|
|
2443
|
+
* Get an `IInferenceEngine` handle. Without `modelId`, returns the
|
|
2444
|
+
* active model for the step (camera-runtime hot path). Pass
|
|
2445
|
+
* `modelId` to grab a specific warm variant (benchmark path).
|
|
2446
|
+
* Touches the LRU clock so the looked-up model survives eviction
|
|
2447
|
+
* pressure from concurrent loads.
|
|
2448
|
+
*/
|
|
2449
|
+
getHandle(stepId, modelId) {
|
|
2450
|
+
const entry = this.resolveOrThrow(stepId, modelId);
|
|
2451
|
+
this.touch(entry);
|
|
2452
|
+
return this.pool.getHandle(entry.poolIndex);
|
|
2453
|
+
}
|
|
2454
|
+
/** True iff the step has any model loaded. */
|
|
2455
|
+
isLoaded(stepId) {
|
|
2456
|
+
return this.loaded.has(stepId) && (this.loaded.get(stepId)?.size ?? 0) > 0;
|
|
2457
|
+
}
|
|
2458
|
+
/** True iff the specific (stepId, modelId) pair is resident. */
|
|
2459
|
+
isLoadedWithModel(stepId, modelId) {
|
|
2460
|
+
return this.loaded.get(stepId)?.has(modelId) ?? false;
|
|
2461
|
+
}
|
|
2462
|
+
/** ModelId currently designated as active for the step. */
|
|
2463
|
+
getLoadedModelId(stepId) {
|
|
2464
|
+
return this.activeByStep.get(stepId);
|
|
2465
|
+
}
|
|
2466
|
+
/**
|
|
2467
|
+
* Pool index for a given (stepId, modelId). Without `modelId`,
|
|
2468
|
+
* returns the active model's index. Used by the inference fast
|
|
2469
|
+
* paths that bypass `getHandle` and call `pool.inferBatch` directly.
|
|
2470
|
+
*/
|
|
2471
|
+
getPoolIndex(stepId, modelId) {
|
|
2472
|
+
const entry = this.resolve(stepId, modelId);
|
|
2473
|
+
if (!entry) return null;
|
|
2474
|
+
this.touch(entry);
|
|
2475
|
+
return entry.poolIndex;
|
|
2476
|
+
}
|
|
2477
|
+
/**
|
|
2478
|
+
* Direct access to the underlying pool — exposed so the provider can
|
|
2479
|
+
* call `inferBatch` without re-fetching individual handles. Pool
|
|
2480
|
+
* lifetime is owned by `EngineFactory`, so callers must not dispose.
|
|
2481
|
+
*/
|
|
2482
|
+
getPool() {
|
|
2483
|
+
return this.pool;
|
|
2484
|
+
}
|
|
2485
|
+
/**
|
|
2486
|
+
* Snapshot of every (stepId, modelId, poolIndex) currently resident.
|
|
2487
|
+
* Used by the `Warm instances` UI tab + cap-level `listLoadedEngines`
|
|
2488
|
+
* to surface both the runtime model and the warm extras.
|
|
2489
|
+
*/
|
|
2490
|
+
getLoadedSteps() {
|
|
2491
|
+
const out = [];
|
|
2492
|
+
for (const [stepId, models] of this.loaded) {
|
|
2493
|
+
const activeModelId = this.activeByStep.get(stepId);
|
|
2494
|
+
for (const entry of models.values()) out.push({
|
|
2495
|
+
stepId: entry.stepId,
|
|
2496
|
+
modelId: entry.modelId,
|
|
2497
|
+
poolIndex: entry.poolIndex,
|
|
2498
|
+
active: entry.modelId === activeModelId
|
|
2499
|
+
});
|
|
1908
2500
|
}
|
|
2501
|
+
return out;
|
|
1909
2502
|
}
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
}],
|
|
1924
|
-
preprocessMode: "resize",
|
|
1925
|
-
inputNormalization: "none",
|
|
1926
|
-
formats: {
|
|
1927
|
-
onnx: {
|
|
1928
|
-
url: hf("clip/mobileclip-s0/onnx/camstack-mobileclip-s0-vision.onnx"),
|
|
1929
|
-
sizeMB: 40
|
|
1930
|
-
},
|
|
1931
|
-
openvino: ovFormat(hf("clip/mobileclip-s0/openvino/camstack-mobileclip-s0-vision.xml"), 25),
|
|
1932
|
-
coreml: {
|
|
1933
|
-
url: hf("clip/mobileclip-s0/coreml/camstack-mobileclip-s0-vision.mlpackage"),
|
|
1934
|
-
sizeMB: 30,
|
|
1935
|
-
isDirectory: true,
|
|
1936
|
-
files: [...MLPACKAGE_FILES],
|
|
1937
|
-
runtimes: ["python"]
|
|
1938
|
-
}
|
|
2503
|
+
/**
|
|
2504
|
+
* Load extra models without touching the active runtime model.
|
|
2505
|
+
* Idempotent — already-loaded (stepId, modelId) pairs are no-op'd.
|
|
2506
|
+
* If `maxModelsPerStep` is exceeded, the LRU non-active variant is
|
|
2507
|
+
* evicted (NOT the active one — that would knock the runtime cameras
|
|
2508
|
+
* offline). The "active" designation is unchanged: callers without
|
|
2509
|
+
* a `modelId` argument keep hitting the runtime model.
|
|
2510
|
+
*/
|
|
2511
|
+
async loadAdditional(steps) {
|
|
2512
|
+
for (const step of steps) {
|
|
2513
|
+
if (this.isLoadedWithModel(step.addonId, step.modelId)) continue;
|
|
2514
|
+
await this.ensureLoaded(step.addonId, step.modelId);
|
|
2515
|
+
if (!this.activeByStep.has(step.addonId)) this.activeByStep.set(step.addonId, step.modelId);
|
|
1939
2516
|
}
|
|
1940
|
-
}
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
name: "CLIP Embedding"
|
|
1952
|
-
}],
|
|
1953
|
-
preprocessMode: "resize",
|
|
1954
|
-
inputNormalization: "none",
|
|
1955
|
-
formats: {
|
|
1956
|
-
onnx: {
|
|
1957
|
-
url: hf("clip/mobileclip-s1/onnx/camstack-mobileclip-s1-vision.onnx"),
|
|
1958
|
-
sizeMB: 90
|
|
1959
|
-
},
|
|
1960
|
-
openvino: ovFormat(hf("clip/mobileclip-s1/openvino/camstack-mobileclip-s1-vision.xml"), 55),
|
|
1961
|
-
coreml: {
|
|
1962
|
-
url: hf("clip/mobileclip-s1/coreml/camstack-mobileclip-s1-vision.mlpackage"),
|
|
1963
|
-
sizeMB: 65,
|
|
1964
|
-
isDirectory: true,
|
|
1965
|
-
files: [...MLPACKAGE_FILES],
|
|
1966
|
-
runtimes: ["python"]
|
|
1967
|
-
}
|
|
2517
|
+
}
|
|
2518
|
+
/**
|
|
2519
|
+
* Load `(stepId, modelId)` into the pool if not already resident.
|
|
2520
|
+
* Honors `maxModelsPerStep` — evicts a non-active LRU variant first
|
|
2521
|
+
* when the cap would otherwise be exceeded.
|
|
2522
|
+
*/
|
|
2523
|
+
async ensureLoaded(stepId, modelId) {
|
|
2524
|
+
let perStep = this.loaded.get(stepId);
|
|
2525
|
+
if (!perStep) {
|
|
2526
|
+
perStep = /* @__PURE__ */ new Map();
|
|
2527
|
+
this.loaded.set(stepId, perStep);
|
|
1968
2528
|
}
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
formats: {
|
|
1985
|
-
onnx: {
|
|
1986
|
-
url: hf("clip/mobileclip-s2/onnx/camstack-mobileclip-s2-vision.onnx"),
|
|
1987
|
-
sizeMB: 150
|
|
1988
|
-
},
|
|
1989
|
-
openvino: ovFormat(hf("clip/mobileclip-s2/openvino/camstack-mobileclip-s2-vision.xml"), 90),
|
|
1990
|
-
coreml: {
|
|
1991
|
-
url: hf("clip/mobileclip-s2/coreml/camstack-mobileclip-s2-vision.mlpackage"),
|
|
1992
|
-
sizeMB: 110,
|
|
1993
|
-
isDirectory: true,
|
|
1994
|
-
files: [...MLPACKAGE_FILES],
|
|
1995
|
-
runtimes: ["python"]
|
|
1996
|
-
}
|
|
2529
|
+
const existing = perStep.get(modelId);
|
|
2530
|
+
if (existing) {
|
|
2531
|
+
this.touch(existing);
|
|
2532
|
+
return existing;
|
|
2533
|
+
}
|
|
2534
|
+
while (perStep.size >= this.maxModelsPerStep) {
|
|
2535
|
+
const evicted = this.pickEvictionTarget(stepId);
|
|
2536
|
+
if (!evicted) break;
|
|
2537
|
+
await this.unloadEntry(evicted);
|
|
2538
|
+
this.log.info("LRU evicted warm model", { meta: {
|
|
2539
|
+
step: stepId,
|
|
2540
|
+
modelId: evicted.modelId,
|
|
2541
|
+
poolIndex: evicted.poolIndex,
|
|
2542
|
+
cap: this.maxModelsPerStep
|
|
2543
|
+
} });
|
|
1997
2544
|
}
|
|
2545
|
+
const index = this.pool.allocateIndex();
|
|
2546
|
+
const config = this.buildPoolConfig(stepId, modelId);
|
|
2547
|
+
this.log.info("Loading step variant", { meta: {
|
|
2548
|
+
step: stepId,
|
|
2549
|
+
modelId,
|
|
2550
|
+
poolIndex: index
|
|
2551
|
+
} });
|
|
2552
|
+
const { loadMs } = await this.pool.loadModel(index, config);
|
|
2553
|
+
this.log.info("Step variant loaded", { meta: {
|
|
2554
|
+
step: stepId,
|
|
2555
|
+
modelId,
|
|
2556
|
+
poolIndex: index,
|
|
2557
|
+
loadMs
|
|
2558
|
+
} });
|
|
2559
|
+
const entry = {
|
|
2560
|
+
stepId,
|
|
2561
|
+
modelId,
|
|
2562
|
+
poolIndex: index,
|
|
2563
|
+
lruTick: ++this.lruClock
|
|
2564
|
+
};
|
|
2565
|
+
perStep.set(modelId, entry);
|
|
2566
|
+
return entry;
|
|
1998
2567
|
}
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
height: 16e3
|
|
2007
|
-
},
|
|
2008
|
-
labels: [],
|
|
2009
|
-
preprocessMode: "resize",
|
|
2010
|
-
formats: {
|
|
2011
|
-
onnx: {
|
|
2012
|
-
url: hf("audioClassification/yamnet/onnx/camstack-yamnet.onnx"),
|
|
2013
|
-
sizeMB: 3.2
|
|
2014
|
-
},
|
|
2015
|
-
openvino: ovFormat(hf("audioClassification/yamnet/openvino/camstack-yamnet.xml"), 3.2),
|
|
2016
|
-
coreml: {
|
|
2017
|
-
url: hf("audioClassification/yamnet/onnx/camstack-yamnet.onnx"),
|
|
2018
|
-
sizeMB: 3.2,
|
|
2019
|
-
runtimes: ["python"]
|
|
2568
|
+
async unloadEntry(entry) {
|
|
2569
|
+
await this.pool.unloadModel(entry.poolIndex);
|
|
2570
|
+
const perStep = this.loaded.get(entry.stepId);
|
|
2571
|
+
perStep?.delete(entry.modelId);
|
|
2572
|
+
if (perStep && perStep.size === 0) {
|
|
2573
|
+
this.loaded.delete(entry.stepId);
|
|
2574
|
+
this.activeByStep.delete(entry.stepId);
|
|
2020
2575
|
}
|
|
2021
2576
|
}
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
url: "",
|
|
2034
|
-
sizeMB: 0
|
|
2035
|
-
} }
|
|
2036
|
-
}];
|
|
2037
|
-
//#endregion
|
|
2038
|
-
//#region src/detection-pipeline/registry/step-definitions.ts
|
|
2039
|
-
var ANIMAL_10_LABELS = [
|
|
2040
|
-
"cat",
|
|
2041
|
-
"cow",
|
|
2042
|
-
"dog",
|
|
2043
|
-
"dolphin",
|
|
2044
|
-
"eagle",
|
|
2045
|
-
"giant panda",
|
|
2046
|
-
"horse",
|
|
2047
|
-
"monkey",
|
|
2048
|
-
"sheep",
|
|
2049
|
-
"spider"
|
|
2050
|
-
];
|
|
2051
|
-
var VGG_G2_CHARSET = ["", ..."0123456789!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split("")];
|
|
2052
|
-
var PipelineStepBase = class {
|
|
2053
|
-
definition;
|
|
2054
|
-
constructor(definition) {
|
|
2055
|
-
this.definition = definition;
|
|
2577
|
+
/** Select the LRU non-active entry for a step. Returns null when only the active one is present. */
|
|
2578
|
+
pickEvictionTarget(stepId) {
|
|
2579
|
+
const perStep = this.loaded.get(stepId);
|
|
2580
|
+
if (!perStep) return null;
|
|
2581
|
+
const activeModelId = this.activeByStep.get(stepId);
|
|
2582
|
+
let oldest = null;
|
|
2583
|
+
for (const entry of perStep.values()) {
|
|
2584
|
+
if (entry.modelId === activeModelId) continue;
|
|
2585
|
+
if (!oldest || entry.lruTick < oldest.lruTick) oldest = entry;
|
|
2586
|
+
}
|
|
2587
|
+
return oldest;
|
|
2056
2588
|
}
|
|
2057
|
-
|
|
2058
|
-
|
|
2589
|
+
/** Trim the step down to `maxModelsPerStep` entries, evicting LRU non-actives. */
|
|
2590
|
+
async enforceLruCap(stepId) {
|
|
2591
|
+
const perStep = this.loaded.get(stepId);
|
|
2592
|
+
if (!perStep) return;
|
|
2593
|
+
while (perStep.size > this.maxModelsPerStep) {
|
|
2594
|
+
const evicted = this.pickEvictionTarget(stepId);
|
|
2595
|
+
if (!evicted) break;
|
|
2596
|
+
await this.unloadEntry(evicted);
|
|
2597
|
+
}
|
|
2059
2598
|
}
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
postprocessor: "yolo",
|
|
2067
|
-
extractMode: "full-frame",
|
|
2068
|
-
inputClasses: null,
|
|
2069
|
-
outputClasses: [
|
|
2070
|
-
"person",
|
|
2071
|
-
"vehicle",
|
|
2072
|
-
"animal"
|
|
2073
|
-
],
|
|
2074
|
-
models: [...OBJECT_DETECTION_MODELS],
|
|
2075
|
-
defaultModelId: "yolo26n",
|
|
2076
|
-
defaultConfidence: .5,
|
|
2077
|
-
labels: COCO_80_LABELS.map((l) => l.id),
|
|
2078
|
-
classMap: COCO_TO_MACRO
|
|
2079
|
-
};
|
|
2080
|
-
getConfigSchema() {
|
|
2081
|
-
return [
|
|
2082
|
-
{
|
|
2083
|
-
type: "multiselect",
|
|
2084
|
-
key: "enabledMacroClasses",
|
|
2085
|
-
label: "Enabled Classes",
|
|
2086
|
-
description: "Which macro classes to detect (empty = all three).",
|
|
2087
|
-
options: [
|
|
2088
|
-
{
|
|
2089
|
-
value: "person",
|
|
2090
|
-
label: "Person"
|
|
2091
|
-
},
|
|
2092
|
-
{
|
|
2093
|
-
value: "vehicle",
|
|
2094
|
-
label: "Vehicle"
|
|
2095
|
-
},
|
|
2096
|
-
{
|
|
2097
|
-
value: "animal",
|
|
2098
|
-
label: "Animal"
|
|
2099
|
-
}
|
|
2100
|
-
],
|
|
2101
|
-
default: [
|
|
2102
|
-
"person",
|
|
2103
|
-
"vehicle",
|
|
2104
|
-
"animal"
|
|
2105
|
-
]
|
|
2106
|
-
},
|
|
2107
|
-
{
|
|
2108
|
-
type: "slider",
|
|
2109
|
-
key: "minConfidencePerson",
|
|
2110
|
-
label: "Min Confidence — Person",
|
|
2111
|
-
description: "Drop person predictions below this score.",
|
|
2112
|
-
min: 0,
|
|
2113
|
-
max: 1,
|
|
2114
|
-
step: .05,
|
|
2115
|
-
default: .5,
|
|
2116
|
-
showValue: true
|
|
2117
|
-
},
|
|
2118
|
-
{
|
|
2119
|
-
type: "slider",
|
|
2120
|
-
key: "minConfidenceVehicle",
|
|
2121
|
-
label: "Min Confidence — Vehicle",
|
|
2122
|
-
description: "Drop vehicle predictions below this score.",
|
|
2123
|
-
min: 0,
|
|
2124
|
-
max: 1,
|
|
2125
|
-
step: .05,
|
|
2126
|
-
default: .5,
|
|
2127
|
-
showValue: true
|
|
2128
|
-
},
|
|
2129
|
-
{
|
|
2130
|
-
type: "slider",
|
|
2131
|
-
key: "minConfidenceAnimal",
|
|
2132
|
-
label: "Min Confidence — Animal",
|
|
2133
|
-
description: "Drop animal predictions below this score.",
|
|
2134
|
-
min: 0,
|
|
2135
|
-
max: 1,
|
|
2136
|
-
step: .05,
|
|
2137
|
-
default: .5,
|
|
2138
|
-
showValue: true
|
|
2139
|
-
}
|
|
2140
|
-
];
|
|
2599
|
+
resolve(stepId, modelId) {
|
|
2600
|
+
const perStep = this.loaded.get(stepId);
|
|
2601
|
+
if (!perStep) return null;
|
|
2602
|
+
const targetModelId = modelId ?? this.activeByStep.get(stepId);
|
|
2603
|
+
if (!targetModelId) return null;
|
|
2604
|
+
return perStep.get(targetModelId) ?? null;
|
|
2141
2605
|
}
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
name: "Audio Classifier",
|
|
2147
|
-
slot: "audio-classifier",
|
|
2148
|
-
postprocessor: "yamnet",
|
|
2149
|
-
extractMode: "full-frame",
|
|
2150
|
-
inputClasses: null,
|
|
2151
|
-
outputClasses: ["audio"],
|
|
2152
|
-
models: [...AUDIO_CLASSIFIER_MODELS],
|
|
2153
|
-
defaultModelId: "yamnet-onnx",
|
|
2154
|
-
defaultModelIdByFormat: { coreml: "apple-soundanalysis" },
|
|
2155
|
-
enabledByDefault: true,
|
|
2156
|
-
defaultConfidence: .3
|
|
2157
|
-
};
|
|
2158
|
-
getConfigSchema() {
|
|
2159
|
-
return [{
|
|
2160
|
-
type: "multiselect",
|
|
2161
|
-
key: "enabledAudioClasses",
|
|
2162
|
-
label: "Audio Classes",
|
|
2163
|
-
description: "Which audio macro classes to surface (empty = all).",
|
|
2164
|
-
options: AUDIO_MACRO_LABELS.map((l) => ({
|
|
2165
|
-
value: l.id,
|
|
2166
|
-
label: `${l.icon ?? ""} ${l.name}`.trim()
|
|
2167
|
-
})),
|
|
2168
|
-
default: [
|
|
2169
|
-
"scream",
|
|
2170
|
-
"crying",
|
|
2171
|
-
"dog",
|
|
2172
|
-
"alarm",
|
|
2173
|
-
"doorbell",
|
|
2174
|
-
"glass_breaking",
|
|
2175
|
-
"gunshot",
|
|
2176
|
-
"siren",
|
|
2177
|
-
"footsteps"
|
|
2178
|
-
]
|
|
2179
|
-
}, {
|
|
2180
|
-
type: "slider",
|
|
2181
|
-
key: "minConfidence",
|
|
2182
|
-
label: "Min Confidence",
|
|
2183
|
-
description: "Drop predictions whose score is below this threshold.",
|
|
2184
|
-
default: .3,
|
|
2185
|
-
min: 0,
|
|
2186
|
-
max: 1,
|
|
2187
|
-
step: .05,
|
|
2188
|
-
showValue: true
|
|
2189
|
-
}];
|
|
2606
|
+
resolveOrThrow(stepId, modelId) {
|
|
2607
|
+
const entry = this.resolve(stepId, modelId);
|
|
2608
|
+
if (!entry) throw new Error(`Step "${stepId}"${modelId ? ` (model "${modelId}")` : ""} is not loaded in the inference pool`);
|
|
2609
|
+
return entry;
|
|
2190
2610
|
}
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
definition = {
|
|
2194
|
-
id: "animal-classifier",
|
|
2195
|
-
name: "Animal Classifier",
|
|
2196
|
-
slot: "classifier",
|
|
2197
|
-
postprocessor: "softmax",
|
|
2198
|
-
extractMode: "crop-roi",
|
|
2199
|
-
inputClasses: ["animal"],
|
|
2200
|
-
outputClasses: ["animal-type"],
|
|
2201
|
-
models: [...ANIMAL_CLASSIFIER_MODELS],
|
|
2202
|
-
defaultModelId: "animals-10",
|
|
2203
|
-
enabledByDefault: false,
|
|
2204
|
-
defaultConfidence: .2,
|
|
2205
|
-
labels: [...ANIMAL_10_LABELS]
|
|
2206
|
-
};
|
|
2207
|
-
getConfigSchema() {
|
|
2208
|
-
return [{
|
|
2209
|
-
type: "multiselect",
|
|
2210
|
-
key: "enabledClasses",
|
|
2211
|
-
label: "Species",
|
|
2212
|
-
description: "Which animal species to keep (empty = all). The classifier still scores every label but only the selected classes are returned on the detection.",
|
|
2213
|
-
options: ANIMAL_10_LABELS.map((l) => ({
|
|
2214
|
-
value: l,
|
|
2215
|
-
label: l
|
|
2216
|
-
})),
|
|
2217
|
-
default: []
|
|
2218
|
-
}, {
|
|
2219
|
-
type: "slider",
|
|
2220
|
-
key: "minConfidence",
|
|
2221
|
-
label: "Min Confidence",
|
|
2222
|
-
description: "Drop classifications whose score is below this threshold.",
|
|
2223
|
-
default: this.definition.defaultConfidence,
|
|
2224
|
-
min: 0,
|
|
2225
|
-
max: 1,
|
|
2226
|
-
step: .05,
|
|
2227
|
-
showValue: true
|
|
2228
|
-
}];
|
|
2611
|
+
touch(entry) {
|
|
2612
|
+
entry.lruTick = ++this.lruClock;
|
|
2229
2613
|
}
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2614
|
+
/** Compute diff between currently-active steps and desired steps. */
|
|
2615
|
+
computeDiff(desiredSteps) {
|
|
2616
|
+
const desiredMap = /* @__PURE__ */ new Map();
|
|
2617
|
+
for (const step of desiredSteps) desiredMap.set(step.addonId, step);
|
|
2618
|
+
const added = [];
|
|
2619
|
+
const removed = [];
|
|
2620
|
+
const modelChanged = [];
|
|
2621
|
+
const unchanged = [];
|
|
2622
|
+
for (const [stepId, activeModelId] of this.activeByStep) {
|
|
2623
|
+
const activeEntry = this.loaded.get(stepId)?.get(activeModelId);
|
|
2624
|
+
if (!activeEntry) continue;
|
|
2625
|
+
const desired = desiredMap.get(stepId);
|
|
2626
|
+
if (!desired) {
|
|
2627
|
+
const perStep = this.loaded.get(stepId);
|
|
2628
|
+
if (perStep) for (const entry of perStep.values()) removed.push(entry);
|
|
2629
|
+
} else if (desired.modelId !== activeModelId) modelChanged.push({
|
|
2630
|
+
step: desired,
|
|
2631
|
+
oldModelId: activeModelId
|
|
2632
|
+
});
|
|
2633
|
+
else unchanged.push(activeEntry);
|
|
2634
|
+
}
|
|
2635
|
+
for (const [stepId, step] of desiredMap) if (!this.activeByStep.has(stepId)) added.push(step);
|
|
2636
|
+
return {
|
|
2637
|
+
added,
|
|
2638
|
+
removed,
|
|
2639
|
+
modelChanged,
|
|
2640
|
+
unchanged
|
|
2641
|
+
};
|
|
2244
2642
|
}
|
|
2245
2643
|
};
|
|
2246
|
-
var STEP_FACE_DETECTION = new PipelineStepBase({
|
|
2247
|
-
id: "face-detection",
|
|
2248
|
-
name: "Face Detection",
|
|
2249
|
-
slot: "cropper",
|
|
2250
|
-
postprocessor: "scrfd",
|
|
2251
|
-
extractMode: "crop-roi",
|
|
2252
|
-
inputClasses: ["person"],
|
|
2253
|
-
outputClasses: ["face"],
|
|
2254
|
-
models: [...FACE_DETECTION_MODELS],
|
|
2255
|
-
defaultModelId: "scrfd-2.5g",
|
|
2256
|
-
defaultConfidence: .5,
|
|
2257
|
-
defaultMinParentScore: .7
|
|
2258
|
-
});
|
|
2259
|
-
var STEP_FACE_EMBEDDING = new PipelineStepBase({
|
|
2260
|
-
id: "face-embedding",
|
|
2261
|
-
name: "Face Embedding",
|
|
2262
|
-
slot: "classifier",
|
|
2263
|
-
postprocessor: "arcface",
|
|
2264
|
-
extractMode: "crop-roi",
|
|
2265
|
-
inputClasses: ["face"],
|
|
2266
|
-
outputClasses: ["identity"],
|
|
2267
|
-
models: [...FACE_EMBEDDING_MODELS],
|
|
2268
|
-
defaultModelId: "arcface-r100",
|
|
2269
|
-
defaultConfidence: 0
|
|
2270
|
-
});
|
|
2271
|
-
var STEP_CLIP_EMBEDDING = new PipelineStepBase({
|
|
2272
|
-
id: "clip-embedding",
|
|
2273
|
-
name: "CLIP Embedding",
|
|
2274
|
-
slot: "classifier",
|
|
2275
|
-
postprocessor: "clip",
|
|
2276
|
-
extractMode: "crop-roi",
|
|
2277
|
-
inputClasses: null,
|
|
2278
|
-
outputClasses: ["embedding"],
|
|
2279
|
-
models: [...CLIP_EMBEDDING_MODELS],
|
|
2280
|
-
defaultModelId: "mobileclip-s1",
|
|
2281
|
-
enabledByDefault: false,
|
|
2282
|
-
defaultConfidence: 0,
|
|
2283
|
-
defaultMinParentScore: .4,
|
|
2284
|
-
group: "CLIP"
|
|
2285
|
-
});
|
|
2286
|
-
var STEP_PLATE_DETECTION = new PipelineStepBase({
|
|
2287
|
-
id: "plate-detection",
|
|
2288
|
-
name: "Plate Detection",
|
|
2289
|
-
slot: "cropper",
|
|
2290
|
-
postprocessor: "yolo",
|
|
2291
|
-
extractMode: "crop-roi",
|
|
2292
|
-
inputClasses: ["vehicle"],
|
|
2293
|
-
outputClasses: ["plate"],
|
|
2294
|
-
models: [...PLATE_DETECTION_MODELS],
|
|
2295
|
-
defaultModelId: "yolov8n-plate",
|
|
2296
|
-
defaultConfidence: .5,
|
|
2297
|
-
defaultMinParentScore: .7,
|
|
2298
|
-
labels: ["plate"]
|
|
2299
|
-
});
|
|
2300
|
-
var STEP_PLATE_OCR = new PipelineStepBase({
|
|
2301
|
-
id: "plate-ocr",
|
|
2302
|
-
name: "Plate OCR",
|
|
2303
|
-
slot: "classifier",
|
|
2304
|
-
postprocessor: "ctc",
|
|
2305
|
-
extractMode: "crop-roi",
|
|
2306
|
-
inputClasses: ["plate"],
|
|
2307
|
-
outputClasses: ["plate-text"],
|
|
2308
|
-
models: [...PLATE_OCR_MODELS],
|
|
2309
|
-
defaultModelId: "vgg-english-g2",
|
|
2310
|
-
defaultConfidence: 0,
|
|
2311
|
-
charset: [...VGG_G2_CHARSET]
|
|
2312
|
-
});
|
|
2313
|
-
var STEP_BIRD_CLASSIFIER = new ClassifierWithMinConfidence({
|
|
2314
|
-
id: "bird-classifier",
|
|
2315
|
-
name: "Bird Classifier",
|
|
2316
|
-
slot: "classifier",
|
|
2317
|
-
postprocessor: "softmax",
|
|
2318
|
-
extractMode: "crop-roi",
|
|
2319
|
-
inputClasses: ["animal"],
|
|
2320
|
-
outputClasses: ["species"],
|
|
2321
|
-
models: [...BIRD_CLASSIFIER_MODELS],
|
|
2322
|
-
defaultModelId: "bird-nabirds-404",
|
|
2323
|
-
enabledByDefault: false,
|
|
2324
|
-
defaultConfidence: .2
|
|
2325
|
-
});
|
|
2326
|
-
var STEP_VEHICLE_CLASSIFIER = new ClassifierWithMinConfidence({
|
|
2327
|
-
id: "vehicle-classifier",
|
|
2328
|
-
name: "Vehicle Classifier",
|
|
2329
|
-
slot: "classifier",
|
|
2330
|
-
postprocessor: "softmax",
|
|
2331
|
-
extractMode: "crop-roi",
|
|
2332
|
-
inputClasses: ["vehicle"],
|
|
2333
|
-
outputClasses: ["vehicle-type"],
|
|
2334
|
-
models: [...VEHICLE_CLASSIFIER_MODELS],
|
|
2335
|
-
defaultModelId: "vehicle-type-efficientnet",
|
|
2336
|
-
enabledByDefault: false,
|
|
2337
|
-
defaultConfidence: .3
|
|
2338
|
-
});
|
|
2339
|
-
var STEP_SEGMENTATION = new PipelineStepBase({
|
|
2340
|
-
id: "segmentation",
|
|
2341
|
-
name: "Segmentation",
|
|
2342
|
-
slot: "refiner",
|
|
2343
|
-
postprocessor: "saliency",
|
|
2344
|
-
extractMode: "crop-roi",
|
|
2345
|
-
inputClasses: null,
|
|
2346
|
-
outputClasses: ["mask"],
|
|
2347
|
-
models: [...SEGMENTATION_REFINER_MODELS],
|
|
2348
|
-
defaultModelId: "u2netp",
|
|
2349
|
-
enabledByDefault: false,
|
|
2350
|
-
defaultConfidence: 0,
|
|
2351
|
-
group: "Segmentation"
|
|
2352
|
-
});
|
|
2353
|
-
new PipelineStepBase({
|
|
2354
|
-
id: "instance-segmentation",
|
|
2355
|
-
name: "Instance Segmentation",
|
|
2356
|
-
slot: "refiner",
|
|
2357
|
-
postprocessor: "yolo-seg",
|
|
2358
|
-
extractMode: "full-frame",
|
|
2359
|
-
inputClasses: null,
|
|
2360
|
-
outputClasses: ["mask"],
|
|
2361
|
-
models: [...INSTANCE_SEGMENTATION_MODELS],
|
|
2362
|
-
defaultModelId: "yolo26n-seg",
|
|
2363
|
-
enabledByDefault: false,
|
|
2364
|
-
defaultConfidence: .25,
|
|
2365
|
-
labels: COCO_80_LABELS.map((l) => l.id),
|
|
2366
|
-
classMap: COCO_TO_MACRO,
|
|
2367
|
-
group: "Segmentation"
|
|
2368
|
-
});
|
|
2369
|
-
var STEP_OBJECT_DETECTION_INSTANCE = new ObjectDetectionStep();
|
|
2370
|
-
var STEP_AUDIO_CLASSIFIER_INSTANCE = new AudioClassifierStep();
|
|
2371
|
-
var ALL_PIPELINE_STEPS = [
|
|
2372
|
-
STEP_OBJECT_DETECTION_INSTANCE,
|
|
2373
|
-
STEP_FACE_DETECTION,
|
|
2374
|
-
STEP_FACE_EMBEDDING,
|
|
2375
|
-
STEP_CLIP_EMBEDDING,
|
|
2376
|
-
STEP_PLATE_DETECTION,
|
|
2377
|
-
STEP_PLATE_OCR,
|
|
2378
|
-
new AnimalClassifierStep(),
|
|
2379
|
-
STEP_BIRD_CLASSIFIER,
|
|
2380
|
-
STEP_VEHICLE_CLASSIFIER,
|
|
2381
|
-
STEP_SEGMENTATION,
|
|
2382
|
-
STEP_AUDIO_CLASSIFIER_INSTANCE
|
|
2383
|
-
];
|
|
2384
|
-
/** Compat: flat array of StepDefinition for existing consumers */
|
|
2385
|
-
var ALL_STEPS = ALL_PIPELINE_STEPS.map((s) => s.definition);
|
|
2386
|
-
var STEP_MAP = new Map(ALL_PIPELINE_STEPS.map((s) => [s.definition.id, s]));
|
|
2387
|
-
/**
|
|
2388
|
-
* Look up a pipeline step by ID (class instance with getConfigSchema).
|
|
2389
|
-
* @throws if the step ID is not registered.
|
|
2390
|
-
*/
|
|
2391
|
-
function getStep(stepId) {
|
|
2392
|
-
const step = STEP_MAP.get(stepId);
|
|
2393
|
-
if (!step) throw new Error(`Unknown pipeline step: "${stepId}". Available: ${ALL_PIPELINE_STEPS.map((s) => s.definition.id).join(", ")}`);
|
|
2394
|
-
return step;
|
|
2395
|
-
}
|
|
2396
|
-
/**
|
|
2397
|
-
* Look up a step definition by ID (compat shortcut).
|
|
2398
|
-
* @throws if the step ID is not registered.
|
|
2399
|
-
*/
|
|
2400
|
-
function getStepDefinition(stepId) {
|
|
2401
|
-
return getStep(stepId).definition;
|
|
2402
|
-
}
|
|
2403
|
-
/**
|
|
2404
|
-
* Get the default model ID for a step given the current model format.
|
|
2405
|
-
*
|
|
2406
|
-
* Resolution order:
|
|
2407
|
-
* 1. `def.defaultModelIdByFormat?.[format]` — the step's declared
|
|
2408
|
-
* per-format preference — if that model exists in `def.models` AND
|
|
2409
|
-
* has a build for `format`.
|
|
2410
|
-
* 2. `def.defaultModelId` — the step's plain declared default — if it
|
|
2411
|
-
* exists in `def.models` AND has a build for `format`.
|
|
2412
|
-
* 3. The smallest-by-size model among those with a `format` build
|
|
2413
|
-
* (legacy fallback, preserved for steps/formats with no declared
|
|
2414
|
-
* preference reachable).
|
|
2415
|
-
* 4. `def.defaultModelId` unchanged, when ZERO models have a `format`
|
|
2416
|
-
* build — an unloadable case flagged elsewhere, not resolved here.
|
|
2417
|
-
*/
|
|
2418
|
-
function getDefaultModelForFormat(stepId, format) {
|
|
2419
|
-
const def = getStepDefinition(stepId);
|
|
2420
|
-
const hasFormatBuild = (modelId) => def.models.find((m) => m.id === modelId)?.formats[format] !== void 0;
|
|
2421
|
-
const declaredForFormat = def.defaultModelIdByFormat?.[format];
|
|
2422
|
-
if (declaredForFormat !== void 0 && hasFormatBuild(declaredForFormat)) return declaredForFormat;
|
|
2423
|
-
if (hasFormatBuild(def.defaultModelId)) return def.defaultModelId;
|
|
2424
|
-
const available = def.models.filter((m) => m.formats[format] && m.legacy !== true);
|
|
2425
|
-
if (available.length === 0) return def.defaultModelId;
|
|
2426
|
-
return [...available].toSorted((a, b) => {
|
|
2427
|
-
return (a.formats[format]?.sizeMB ?? Infinity) - (b.formats[format]?.sizeMB ?? Infinity);
|
|
2428
|
-
})[0].id;
|
|
2429
|
-
}
|
|
2430
|
-
/**
|
|
2431
|
-
* The single node-local authority for turning a CHOSEN model (or none) into a
|
|
2432
|
-
* concrete model that has a build for this node's engine `format`. Fuses the
|
|
2433
|
-
* two previously-duplicated decisions (orchestrator `pickDefaultModelId` +
|
|
2434
|
-
* provider `substituteIncompatibleModels`):
|
|
2435
|
-
*
|
|
2436
|
-
* - chosen === null → the addon's default model for `format`.
|
|
2437
|
-
* - chosen is a catalog model → keep it if it has a `format` build; else
|
|
2438
|
-
* the addon's default model for `format`
|
|
2439
|
-
* (a substitution — the caller surfaces it).
|
|
2440
|
-
* - chosen is NOT in the catalog (custom/unknown) → returned unchanged; the
|
|
2441
|
-
* operator owns a custom model's format compat.
|
|
2442
|
-
* - unknown stepId → chosen returned unchanged (never throws).
|
|
2443
|
-
*/
|
|
2444
|
-
function resolveModelForFormat(stepId, chosenModelId, format) {
|
|
2445
|
-
let def;
|
|
2446
|
-
try {
|
|
2447
|
-
def = getStepDefinition(stepId);
|
|
2448
|
-
} catch {
|
|
2449
|
-
return chosenModelId ?? "";
|
|
2450
|
-
}
|
|
2451
|
-
if (chosenModelId === null) return getDefaultModelForFormat(stepId, format);
|
|
2452
|
-
const entry = def.models.find((m) => m.id === chosenModelId);
|
|
2453
|
-
if (!entry) return chosenModelId;
|
|
2454
|
-
if (entry.formats[format]) return chosenModelId;
|
|
2455
|
-
return getDefaultModelForFormat(stepId, format);
|
|
2456
|
-
}
|
|
2457
2644
|
//#endregion
|
|
2458
2645
|
//#region src/detection-pipeline/engine/engine-factory.ts
|
|
2459
2646
|
var BACKEND_TO_POOL_RUNTIME = {
|
|
@@ -4786,6 +4973,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4786
4973
|
writeStore;
|
|
4787
4974
|
/** Read per-device settings. */
|
|
4788
4975
|
readDeviceStore;
|
|
4976
|
+
/** Delete all per-device settings for a device (used by `resetToDefault`). */
|
|
4977
|
+
clearDeviceStore;
|
|
4789
4978
|
constructor(settings, modelsDir, logger, eventBus = null, detectionConfigSchemaSource = null, executorOptions = {}) {
|
|
4790
4979
|
this.modelsDir = modelsDir;
|
|
4791
4980
|
this.eventBus = eventBus;
|
|
@@ -4795,6 +4984,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4795
4984
|
this.readStore = () => settings.readAddonStore();
|
|
4796
4985
|
this.writeStore = (patch) => settings.writeAddonStore(patch);
|
|
4797
4986
|
this.readDeviceStore = settings.readDeviceStore ?? (async () => ({}));
|
|
4987
|
+
const clearDeviceStore = settings.clearDeviceStore;
|
|
4988
|
+
this.clearDeviceStore = clearDeviceStore ? (deviceId) => clearDeviceStore.call(settings, deviceId) : async () => void 0;
|
|
4798
4989
|
this.currentEngine = ONNX_FLOOR;
|
|
4799
4990
|
this.log.info("Engine pick pending (placeholder until probe / persisted selection)", { meta: {
|
|
4800
4991
|
runtime: this.currentEngine.runtime,
|
|
@@ -5261,7 +5452,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5261
5452
|
};
|
|
5262
5453
|
}
|
|
5263
5454
|
async getDefaultSteps(engine) {
|
|
5264
|
-
|
|
5455
|
+
const detectionModelId = resolveDefaultDetectionModel(engine.backend, engine.device ?? null, engine.format);
|
|
5456
|
+
return buildDefaultStepTree(engine.format, detectionModelId);
|
|
5265
5457
|
}
|
|
5266
5458
|
/**
|
|
5267
5459
|
* Resolve every step's model for this node's engine format via the shared
|
|
@@ -5308,7 +5500,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5308
5500
|
if (this.currentSteps) return this.currentSteps;
|
|
5309
5501
|
const raw = (await this.readStore())[KEY_STEPS];
|
|
5310
5502
|
if (!raw) {
|
|
5311
|
-
const
|
|
5503
|
+
const detectionModelId = resolveDefaultDetectionModel(this.currentEngine.backend, this.currentEngine.device ?? null, this.currentEngine.format);
|
|
5504
|
+
const defaults = buildDefaultStepTree(this.currentEngine.format, detectionModelId);
|
|
5312
5505
|
if (defaults.length === 0) return null;
|
|
5313
5506
|
this.currentSteps = this.resolveStepModels(defaults);
|
|
5314
5507
|
this.writeStore({ [KEY_STEPS]: JSON.stringify(defaults) });
|
|
@@ -6781,6 +6974,82 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6781
6974
|
this.log.info("videoPipelineSteps updated", { meta: { addons: Object.keys(sanitized).length } });
|
|
6782
6975
|
return { success: true };
|
|
6783
6976
|
}
|
|
6977
|
+
/**
|
|
6978
|
+
* Reset THIS node's detection-pipeline to its factory defaults, forcing
|
|
6979
|
+
* regeneration of the hardware-aware default object-detection model.
|
|
6980
|
+
*
|
|
6981
|
+
* Node-routed (the cap's default `nodeIdMode: 'routing'`): the generated
|
|
6982
|
+
* cap-router strips `nodeId` and dispatches to the target node, so this
|
|
6983
|
+
* method runs ON the node being reset and takes no argument.
|
|
6984
|
+
*
|
|
6985
|
+
* Clears three things:
|
|
6986
|
+
* 1. the persisted GLOBAL pipeline seed (`KEY_STEPS`) — nulled in the
|
|
6987
|
+
* store AND the in-memory `currentSteps` cache, so the subsequent
|
|
6988
|
+
* `getGlobalSteps()` re-seeds the hardware-aware default tree fresh
|
|
6989
|
+
* (`resolveDefaultDetectionModel` against this node's live engine)
|
|
6990
|
+
* rather than returning the stale cached blob.
|
|
6991
|
+
* 2. the LEGACY engine blob (`KEY_ENGINE`). The per-node engine cascade
|
|
6992
|
+
* (`engineBackend@<node>` / `engineDevice@<node>`) is intentionally
|
|
6993
|
+
* left intact — the operator's engine selection is preserved and the
|
|
6994
|
+
* default is regenerated against it.
|
|
6995
|
+
* 3. every device's PER-CAMERA step overrides (the per-device store the
|
|
6996
|
+
* object-detection root reads via `applyDeviceOverridesToTree`).
|
|
6997
|
+
*
|
|
6998
|
+
* Idempotent: a second call re-seeds the identical default and clears
|
|
6999
|
+
* already-empty per-device stores harmlessly. Returns the regenerated
|
|
7000
|
+
* object-detection model id (null when the default tree has no detector
|
|
7001
|
+
* for this node's format).
|
|
7002
|
+
*/
|
|
7003
|
+
async resetToDefault() {
|
|
7004
|
+
await this.writeStore({
|
|
7005
|
+
[KEY_STEPS]: null,
|
|
7006
|
+
[KEY_ENGINE]: null
|
|
7007
|
+
});
|
|
7008
|
+
this.currentSteps = null;
|
|
7009
|
+
const clearedDevices = await this.clearAllDeviceOverrides();
|
|
7010
|
+
const regeneratedModelId = ((await this.getGlobalSteps())?.find((s) => s.slot === "detector") ?? null)?.modelId ?? null;
|
|
7011
|
+
this.log.info("pipeline reset to default", { meta: {
|
|
7012
|
+
clearedGlobalSteps: true,
|
|
7013
|
+
clearedEngineBlob: true,
|
|
7014
|
+
clearedDeviceOverrides: clearedDevices,
|
|
7015
|
+
regeneratedModelId
|
|
7016
|
+
} });
|
|
7017
|
+
return {
|
|
7018
|
+
success: true,
|
|
7019
|
+
regeneratedModelId
|
|
7020
|
+
};
|
|
7021
|
+
}
|
|
7022
|
+
/**
|
|
7023
|
+
* Clear the per-device pipeline-step overrides for every known device.
|
|
7024
|
+
* Returns the count of devices whose store was cleared. There is no
|
|
7025
|
+
* bulk-list primitive over the per-device store, so devices are
|
|
7026
|
+
* enumerated via the device-manager and cleared one by one
|
|
7027
|
+
* (`clearDeviceStore` is idempotent — a device with no overrides is a
|
|
7028
|
+
* no-op). Never throws: a device-manager outage degrades to "cleared
|
|
7029
|
+
* nothing" so the global reset still completes.
|
|
7030
|
+
*/
|
|
7031
|
+
async clearAllDeviceOverrides() {
|
|
7032
|
+
const api = this.addonCtx?.api;
|
|
7033
|
+
if (!api) return 0;
|
|
7034
|
+
let devices = [];
|
|
7035
|
+
try {
|
|
7036
|
+
devices = await api.deviceManager.listAll.query({});
|
|
7037
|
+
} catch (err) {
|
|
7038
|
+
this.log.warn("resetToDefault: device enumeration failed — skipping per-device clear", { meta: { error: errMsg(err) } });
|
|
7039
|
+
return 0;
|
|
7040
|
+
}
|
|
7041
|
+
let cleared = 0;
|
|
7042
|
+
for (const device of devices) try {
|
|
7043
|
+
await this.clearDeviceStore(device.id);
|
|
7044
|
+
cleared++;
|
|
7045
|
+
} catch (err) {
|
|
7046
|
+
this.log.warn("resetToDefault: failed to clear per-device overrides for device", { meta: {
|
|
7047
|
+
deviceId: device.id,
|
|
7048
|
+
error: errMsg(err)
|
|
7049
|
+
} });
|
|
7050
|
+
}
|
|
7051
|
+
return cleared;
|
|
7052
|
+
}
|
|
6784
7053
|
async listLoadedEngines() {
|
|
6785
7054
|
const out = [];
|
|
6786
7055
|
const overrideFactory = this.overrideCache?.factory ?? null;
|
|
@@ -7106,7 +7375,7 @@ function resolveChildSlots(slot) {
|
|
|
7106
7375
|
* - Covers 90% of NVR use cases (person/vehicle/animal detection + face detection)
|
|
7107
7376
|
* - Users opt-in to heavy classifiers (vehicle type, plate OCR, bird species) when needed
|
|
7108
7377
|
*/
|
|
7109
|
-
function buildDefaultStepTree(format) {
|
|
7378
|
+
function buildDefaultStepTree(format, objectDetectionModelId) {
|
|
7110
7379
|
const makeStep = (stepId, children, opts) => {
|
|
7111
7380
|
const def = getStepDefinition(stepId);
|
|
7112
7381
|
if (!def.models.some((m) => m.formats[format])) return null;
|
|
@@ -7132,7 +7401,7 @@ function buildDefaultStepTree(format) {
|
|
|
7132
7401
|
makeStep("bird-classifier", [], { enabled: false }),
|
|
7133
7402
|
makeStep("vehicle-classifier", [], { enabled: false }),
|
|
7134
7403
|
makeStep("segmentation", [], { enabled: false })
|
|
7135
|
-
].filter((s) => s !== null));
|
|
7404
|
+
].filter((s) => s !== null), objectDetectionModelId !== void 0 ? { modelId: objectDetectionModelId } : void 0);
|
|
7136
7405
|
const audioStep = makeStep("audio-classifier", []);
|
|
7137
7406
|
const steps = [];
|
|
7138
7407
|
if (root) steps.push(root);
|