@camstack/addon-pipeline 1.1.44 → 1.1.46
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 +2308 -2192
- package/dist/detection-pipeline/index.mjs +2309 -2193
- package/dist/{dist-CgBGV988.mjs → dist-Bglbcd7H.mjs} +76 -79
- package/dist/{dist-CySHUkXY.js → dist-C3CWl8fV.js} +76 -79
- 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-B4hRwchu.mjs → remote-source-plane-BrFvA0nx.mjs} +1 -1
- package/dist/{remote-source-plane-D9m0z1N9.js → remote-source-plane-C05A2DHs.js} +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-BGyNZqVy.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-UM9hXKco.mjs +26 -0
- 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-iB6N_WVh.mjs} +1 -1
- package/dist/stream-broker/{hostInit-BSzP3BIj.mjs → hostInit-uMl2Tcss.mjs} +2 -2
- package/dist/stream-broker/index.js +49 -3
- package/dist/stream-broker/index.mjs +49 -3
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-D2ML5TPi.js → MaskShapeCanvas-DI4BY7W2-DvRngVr4.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-TTWGGaqv.js → MotionZonesSettings-NcxxQN8r-ByQeOu3_.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-CJqHU2nU.js → PrivacyMaskSettings-APgPLF7p-Cs5XlrlU.js} +1 -1
- package/embed-dist/assets/{index-CA4kv43T.css → index-JqUY2p33.css} +1 -1
- package/embed-dist/assets/{index-C1oaL9s7.js → index-RqzZkNA8.js} +10 -10
- package/embed-dist/index.html +2 -2
- package/package.json +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Dlagq9X_.mjs +0 -26
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../chunk-D6vf50IK.js");
|
|
6
|
-
const require_dist = require("../dist-
|
|
6
|
+
const require_dist = require("../dist-C3CWl8fV.js");
|
|
7
7
|
const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
|
|
8
8
|
const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-BLXzxpRB.js");
|
|
9
9
|
let _camstack_shm_ring = require("@camstack/shm-ring");
|
|
@@ -298,1059 +298,813 @@ function projectNodeEngine(store, nodeId) {
|
|
|
298
298
|
return out;
|
|
299
299
|
}
|
|
300
300
|
//#endregion
|
|
301
|
-
//#region src/detection-pipeline/
|
|
301
|
+
//#region src/detection-pipeline/registry/model-catalogs.ts
|
|
302
|
+
var HF_REPO = "camstack/camstack-models";
|
|
303
|
+
var HF_SCRYPTED = "scrypted/plugin-models";
|
|
304
|
+
var hf = (path) => require_dist.hfModelUrl(HF_REPO, path);
|
|
305
|
+
var hfScrypted = (path) => require_dist.hfModelUrl(HF_SCRYPTED, path);
|
|
302
306
|
/**
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
* Multi-process orchestrator. Owns N `PoolWorker` instances, each
|
|
306
|
-
* holding its own Python subprocess + MLModel copy, and round-robins
|
|
307
|
-
* inference dispatches across them. Model management (load/unload/
|
|
308
|
-
* replace) propagates to all workers so every subprocess holds the
|
|
309
|
-
* same model set at the same indices.
|
|
307
|
+
* Build an OpenVINO format entry (always python runtime).
|
|
310
308
|
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
var MSG_INFER_RAW = 2;
|
|
318
|
-
var MSG_CACHE_FRAME = 4;
|
|
319
|
-
var MSG_INFER_CACHED = 5;
|
|
320
|
-
/**
|
|
321
|
-
* Cross-camera batching opcode — N items packed into a single IPC
|
|
322
|
-
* frame. Wire shape:
|
|
323
|
-
* [1B model_idx][1B count][N × (4B w | 4B h | 1B fmt | 4B size | raw bytes)]
|
|
324
|
-
* Python pool dispatches each item via `dispatcher.run` (asyncio.gather)
|
|
325
|
-
* and returns a JSON array of detections in the same order.
|
|
326
|
-
*/
|
|
327
|
-
var MSG_INFER_BATCH = 3;
|
|
328
|
-
var PREFIX_LEN = 9;
|
|
329
|
-
/**
|
|
330
|
-
* Wire-level enum for the raw-frame fast path. Values are append-only:
|
|
331
|
-
* the Python pool reads the byte directly off the IPC frame; reordering
|
|
332
|
-
* existing entries breaks all in-flight workers.
|
|
309
|
+
* OpenVINO IR is a two-file bundle: a `.xml` topology + a sibling `.bin`
|
|
310
|
+
* weights file with the same basename. We declare the `.bin` in `files` so
|
|
311
|
+
* the (format-agnostic) downloader fetches it alongside the `.xml` — without
|
|
312
|
+
* the weights, OpenVINO compile fails with "Empty weights data in bin file".
|
|
313
|
+
* A plain `.onnx` run through the OpenVINO runtime (e.g. yamnet) has no
|
|
314
|
+
* sibling, so none is added.
|
|
333
315
|
*/
|
|
334
|
-
var
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
device = "cpu";
|
|
344
|
-
constructor(pool, modelIndex) {
|
|
345
|
-
this.pool = pool;
|
|
346
|
-
this.modelIndex = modelIndex;
|
|
347
|
-
}
|
|
348
|
-
async infer(input) {
|
|
349
|
-
const start = performance.now();
|
|
350
|
-
return {
|
|
351
|
-
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),
|
|
352
|
-
inferenceMs: performance.now() - start
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
/**
|
|
356
|
-
* Inference on a frame previously cached in the Python pool via
|
|
357
|
-
* `pool.cacheFrame()`. Sends only 5 bytes through the pipe instead
|
|
358
|
-
* of 1.2MB raw data — eliminates the pipe transfer bottleneck.
|
|
359
|
-
*/
|
|
360
|
-
async inferFromCache(frameId) {
|
|
361
|
-
const start = performance.now();
|
|
362
|
-
return {
|
|
363
|
-
structured: await this.pool.inferCached(this.modelIndex, frameId),
|
|
364
|
-
inferenceMs: performance.now() - start
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
async dispose() {}
|
|
316
|
+
var ovFormat = (url, sizeMB) => {
|
|
317
|
+
const base = url.split("/").pop() ?? "";
|
|
318
|
+
const files = base.endsWith(".xml") ? [base.replace(/\.xml$/, ".bin")] : void 0;
|
|
319
|
+
return {
|
|
320
|
+
url,
|
|
321
|
+
sizeMB,
|
|
322
|
+
runtimes: ["python"],
|
|
323
|
+
...files ? { files } : {}
|
|
324
|
+
};
|
|
368
325
|
};
|
|
369
326
|
/**
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
327
|
+
* Build a precision-variant catalog entry (OpenVINO-only) derived from a base
|
|
328
|
+
* detection model. fp16 halves the weights (Intel iGPU/NPU sweet spot); int8 is
|
|
329
|
+
* NNCF post-training-quantized (~4× smaller, fastest on CPU/iGPU at a small
|
|
330
|
+
* accuracy cost). The IRs live next to the base `.xml` on HF as
|
|
331
|
+
* `camstack-<id>-<precision>.xml`. Lets an operator scale the model to the node
|
|
332
|
+
* (e.g. yolo26x-int8 on a 265K, yolo26n-int8 on an N100).
|
|
373
333
|
*/
|
|
374
|
-
var
|
|
334
|
+
var ovPrecisionVariant = (baseId, ovDir, baseName, precision, sizeMB, legacy = false) => ({
|
|
335
|
+
id: `${baseId}-${precision}`,
|
|
336
|
+
name: `${baseName} (${precision.toUpperCase()})`,
|
|
337
|
+
description: `${baseName} — OpenVINO ${precision.toUpperCase()} variant for Intel iGPU/NPU; scale by hardware`,
|
|
338
|
+
inputSize: {
|
|
339
|
+
width: 640,
|
|
340
|
+
height: 640
|
|
341
|
+
},
|
|
342
|
+
labels: [],
|
|
343
|
+
preprocessMode: "letterbox",
|
|
344
|
+
formats: { openvino: ovFormat(hf(`${ovDir}/camstack-${baseId}-${precision}.xml`), sizeMB) },
|
|
345
|
+
...legacy ? { legacy: true } : {},
|
|
346
|
+
...precision === "int8" ? { group: {
|
|
347
|
+
family: baseId.slice(0, -1),
|
|
348
|
+
tier: baseId.slice(-1),
|
|
349
|
+
precision: "int8"
|
|
350
|
+
} } : {}
|
|
351
|
+
});
|
|
352
|
+
var MLPACKAGE_FILES = [
|
|
353
|
+
"Manifest.json",
|
|
354
|
+
"Data/com.apple.CoreML/model.mlmodel",
|
|
355
|
+
"Data/com.apple.CoreML/weights/weight.bin"
|
|
356
|
+
];
|
|
357
|
+
var YOLO26_TIER_NAME = {
|
|
358
|
+
n: "Nano",
|
|
359
|
+
s: "Small",
|
|
360
|
+
m: "Medium",
|
|
361
|
+
l: "Large"
|
|
362
|
+
};
|
|
375
363
|
/**
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
* escalation guarantees the process dies. Exported for tests.
|
|
364
|
+
* Reduced-input YOLO26 variants (2026-07-11). A lower square input (320 / 256)
|
|
365
|
+
* is the biggest, cheapest latency lever measured across every runtime — Mac
|
|
366
|
+
* ANE, Intel Ultra NPU, and especially the N100 — at a small-object accuracy
|
|
367
|
+
* cost. Each resolution ships a base (fp32) entry (onnx + coreml + openvino)
|
|
368
|
+
* grouped under `{family, tier, resolution}`, plus an OpenVINO INT8 entry (best
|
|
369
|
+
* on Intel/N100) grouped with `precision:'int8'`. The grouped selector surfaces
|
|
370
|
+
* `resolution` as a variant axis alongside precision.
|
|
384
371
|
*/
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
process = null;
|
|
416
|
-
receiveBuffer = Buffer.alloc(0);
|
|
417
|
-
pending = /* @__PURE__ */ new Map();
|
|
418
|
-
nextRequestId = 1;
|
|
419
|
-
ready = false;
|
|
420
|
-
log;
|
|
421
|
-
opts;
|
|
422
|
-
constructor(opts) {
|
|
423
|
-
this.opts = opts;
|
|
424
|
-
this.log = opts.logger;
|
|
425
|
-
}
|
|
426
|
-
getPid() {
|
|
427
|
-
return this.process?.pid ?? null;
|
|
428
|
-
}
|
|
429
|
-
isReady() {
|
|
430
|
-
return this.ready;
|
|
431
|
-
}
|
|
432
|
-
async initialize(initialModels) {
|
|
433
|
-
this.process = (0, node_child_process.spawn)(this.opts.pythonPath, [this.opts.scriptPath], { stdio: [
|
|
434
|
-
"pipe",
|
|
435
|
-
"pipe",
|
|
436
|
-
"pipe"
|
|
437
|
-
] });
|
|
438
|
-
if (!this.process.stdout || !this.process.stdin) throw new Error("PoolWorker: failed to create process pipes");
|
|
439
|
-
this.process.stderr?.on("data", (chunk) => {
|
|
440
|
-
for (const line of chunk.toString().split("\n")) {
|
|
441
|
-
const trimmed = line.trim();
|
|
442
|
-
if (trimmed) this.log.warn(`[${this.opts.workerLabel}] ${trimmed}`);
|
|
443
|
-
}
|
|
444
|
-
});
|
|
445
|
-
this.process.on("error", (err) => {
|
|
446
|
-
this.log.error("Worker process error", { meta: {
|
|
447
|
-
worker: this.opts.workerLabel,
|
|
448
|
-
error: err.message
|
|
449
|
-
} });
|
|
450
|
-
this.rejectAll(err);
|
|
451
|
-
});
|
|
452
|
-
this.process.on("exit", (code) => {
|
|
453
|
-
if (code !== 0 && code !== null) {
|
|
454
|
-
this.log.error("Worker process exited", { meta: {
|
|
455
|
-
worker: this.opts.workerLabel,
|
|
456
|
-
code
|
|
457
|
-
} });
|
|
458
|
-
this.rejectAll(/* @__PURE__ */ new Error(`Worker process exited with code ${code}`));
|
|
459
|
-
}
|
|
460
|
-
this.ready = false;
|
|
461
|
-
});
|
|
462
|
-
this.process.stdout.on("data", (chunk) => {
|
|
463
|
-
this.receiveBuffer = Buffer.concat([this.receiveBuffer, chunk]);
|
|
464
|
-
this.drainResponses();
|
|
465
|
-
});
|
|
466
|
-
const config = {
|
|
467
|
-
runtime: this.opts.poolRuntime,
|
|
468
|
-
concurrency: this.opts.concurrency,
|
|
469
|
-
models: initialModels.map((m) => serializeModelConfig(m))
|
|
470
|
-
};
|
|
471
|
-
if (this.opts.tuning) {
|
|
472
|
-
config["batch_mode"] = this.opts.tuning.batchMode;
|
|
473
|
-
config["window_ms"] = this.opts.tuning.windowMs;
|
|
474
|
-
config["max_batch_size"] = this.opts.tuning.maxBatchSize;
|
|
475
|
-
config["num_streams"] = this.opts.tuning.numStreams;
|
|
476
|
-
config["intra_op_threads"] = this.opts.tuning.intraOpThreads;
|
|
372
|
+
var yolo26Reduced = (tier, res, sizes) => {
|
|
373
|
+
const name = YOLO26_TIER_NAME[tier] ?? tier.toUpperCase();
|
|
374
|
+
return [{
|
|
375
|
+
id: `yolo26${tier}-${res}`,
|
|
376
|
+
group: {
|
|
377
|
+
family: "yolo26",
|
|
378
|
+
tier,
|
|
379
|
+
resolution: res
|
|
380
|
+
},
|
|
381
|
+
name: `YOLO26 ${name} @${res}`,
|
|
382
|
+
description: `YOLO26 ${name}, reduced ${res}×${res} input — faster on ANE / hub NPU / N100 at a small-object accuracy cost`,
|
|
383
|
+
inputSize: {
|
|
384
|
+
width: res,
|
|
385
|
+
height: res
|
|
386
|
+
},
|
|
387
|
+
labels: [],
|
|
388
|
+
preprocessMode: "letterbox",
|
|
389
|
+
formats: {
|
|
390
|
+
onnx: {
|
|
391
|
+
url: hf(`objectDetection/yolo26/onnx/camstack-yolo26${tier}-${res}.onnx`),
|
|
392
|
+
sizeMB: sizes.onnx
|
|
393
|
+
},
|
|
394
|
+
coreml: {
|
|
395
|
+
url: hf(`objectDetection/yolo26/coreml/camstack-yolo26${tier}-${res}.mlpackage`),
|
|
396
|
+
sizeMB: sizes.coreml,
|
|
397
|
+
isDirectory: true,
|
|
398
|
+
files: [...MLPACKAGE_FILES],
|
|
399
|
+
runtimes: ["python"]
|
|
400
|
+
},
|
|
401
|
+
openvino: ovFormat(hf(`objectDetection/yolo26/openvino/camstack-yolo26${tier}-${res}.xml`), sizes.ovFp32)
|
|
477
402
|
}
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
this.ensureReady();
|
|
523
|
-
if (items.length === 0) return [];
|
|
524
|
-
const headerSize = 2 + items.length * 13;
|
|
525
|
-
const totalRawSize = items.reduce((sum, it) => sum + it.raw.length, 0);
|
|
526
|
-
const payload = Buffer.allocUnsafe(headerSize + totalRawSize);
|
|
527
|
-
payload[0] = modelByte;
|
|
528
|
-
payload[1] = items.length;
|
|
529
|
-
let offset = 2;
|
|
530
|
-
for (const item of items) {
|
|
531
|
-
payload.writeUInt32LE(item.width, offset);
|
|
532
|
-
payload.writeUInt32LE(item.height, offset + 4);
|
|
533
|
-
payload[offset + 8] = RAW_FMT_CODE[item.format];
|
|
534
|
-
payload.writeUInt32LE(item.raw.length, offset + 9);
|
|
535
|
-
offset += 13;
|
|
536
|
-
item.raw.copy(payload, offset);
|
|
537
|
-
offset += item.raw.length;
|
|
403
|
+
}, {
|
|
404
|
+
id: `yolo26${tier}-${res}-int8`,
|
|
405
|
+
group: {
|
|
406
|
+
family: "yolo26",
|
|
407
|
+
tier,
|
|
408
|
+
precision: "int8",
|
|
409
|
+
resolution: res
|
|
410
|
+
},
|
|
411
|
+
name: `YOLO26 ${name} @${res} (INT8)`,
|
|
412
|
+
description: `YOLO26 ${name} @${res} — OpenVINO INT8 for Intel iGPU/CPU; the N100's fastest config`,
|
|
413
|
+
inputSize: {
|
|
414
|
+
width: res,
|
|
415
|
+
height: res
|
|
416
|
+
},
|
|
417
|
+
labels: [],
|
|
418
|
+
preprocessMode: "letterbox",
|
|
419
|
+
formats: { openvino: ovFormat(hf(`objectDetection/yolo26/openvino/camstack-yolo26${tier}-${res}-int8.xml`), sizes.ovInt8) }
|
|
420
|
+
}];
|
|
421
|
+
};
|
|
422
|
+
var OBJECT_DETECTION_MODELS = [
|
|
423
|
+
{
|
|
424
|
+
id: "yolov9t",
|
|
425
|
+
legacy: true,
|
|
426
|
+
name: "YOLOv9 Tiny",
|
|
427
|
+
description: "YOLOv9 Tiny — ultra-lightweight next-gen detector",
|
|
428
|
+
inputSize: {
|
|
429
|
+
width: 640,
|
|
430
|
+
height: 640
|
|
431
|
+
},
|
|
432
|
+
labels: [],
|
|
433
|
+
preprocessMode: "letterbox",
|
|
434
|
+
formats: {
|
|
435
|
+
onnx: {
|
|
436
|
+
url: hf("objectDetection/yolov9/onnx/camstack-yolov9t.onnx"),
|
|
437
|
+
sizeMB: 8
|
|
438
|
+
},
|
|
439
|
+
coreml: {
|
|
440
|
+
url: hf("objectDetection/yolov9/coreml/camstack-yolov9t.mlpackage"),
|
|
441
|
+
sizeMB: 4,
|
|
442
|
+
isDirectory: true,
|
|
443
|
+
files: [...MLPACKAGE_FILES],
|
|
444
|
+
runtimes: ["python"]
|
|
445
|
+
},
|
|
446
|
+
openvino: ovFormat(hf("objectDetection/yolov9/openvino/camstack-yolov9t.xml"), 6)
|
|
538
447
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
this.ensureReady();
|
|
565
|
-
const payload = Buffer.from(JSON.stringify(cmd), "utf8");
|
|
566
|
-
return await this.dispatch(MSG_COMMAND, payload);
|
|
567
|
-
}
|
|
568
|
-
async dispose() {
|
|
569
|
-
const proc = this.process;
|
|
570
|
-
if (!proc) return;
|
|
571
|
-
this.process = null;
|
|
572
|
-
this.ready = false;
|
|
573
|
-
await terminateChild(proc, POOL_WORKER_TERM_GRACE_MS);
|
|
574
|
-
}
|
|
575
|
-
dispatch(msgType, payload) {
|
|
576
|
-
const reqId = this.allocRequestId();
|
|
577
|
-
return new Promise((resolve, reject) => {
|
|
578
|
-
this.pending.set(reqId, {
|
|
579
|
-
resolve,
|
|
580
|
-
reject
|
|
581
|
-
});
|
|
582
|
-
try {
|
|
583
|
-
this.writeFrame(reqId, msgType, payload);
|
|
584
|
-
} catch (err) {
|
|
585
|
-
this.pending.delete(reqId);
|
|
586
|
-
reject(err instanceof Error ? err : new Error(String(err)));
|
|
587
|
-
}
|
|
588
|
-
});
|
|
589
|
-
}
|
|
590
|
-
dispatchMulti(msgType, payloadLen, parts) {
|
|
591
|
-
const reqId = this.allocRequestId();
|
|
592
|
-
return new Promise((resolve, reject) => {
|
|
593
|
-
this.pending.set(reqId, {
|
|
594
|
-
resolve,
|
|
595
|
-
reject
|
|
596
|
-
});
|
|
597
|
-
try {
|
|
598
|
-
if (!this.process?.stdin) throw new Error("PoolWorker: not initialized");
|
|
599
|
-
const prefix = Buffer.allocUnsafe(PREFIX_LEN);
|
|
600
|
-
prefix.writeUInt32LE(5 + payloadLen, 0);
|
|
601
|
-
prefix.writeUInt32LE(reqId, 4);
|
|
602
|
-
prefix[8] = msgType;
|
|
603
|
-
this.process.stdin.write(prefix);
|
|
604
|
-
for (const part of parts) this.process.stdin.write(part);
|
|
605
|
-
} catch (err) {
|
|
606
|
-
this.pending.delete(reqId);
|
|
607
|
-
reject(err instanceof Error ? err : new Error(String(err)));
|
|
608
|
-
}
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
allocRequestId() {
|
|
612
|
-
let id = this.nextRequestId;
|
|
613
|
-
this.nextRequestId = id >= 4294967295 ? 1 : id + 1;
|
|
614
|
-
while (this.pending.has(id)) {
|
|
615
|
-
id = this.nextRequestId;
|
|
616
|
-
this.nextRequestId = id >= 4294967295 ? 1 : id + 1;
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
id: "yolov9s",
|
|
451
|
+
legacy: true,
|
|
452
|
+
name: "YOLOv9 Small",
|
|
453
|
+
description: "YOLOv9 Small — improved efficiency over YOLOv8s",
|
|
454
|
+
inputSize: {
|
|
455
|
+
width: 640,
|
|
456
|
+
height: 640
|
|
457
|
+
},
|
|
458
|
+
labels: [],
|
|
459
|
+
preprocessMode: "letterbox",
|
|
460
|
+
formats: {
|
|
461
|
+
onnx: {
|
|
462
|
+
url: hf("objectDetection/yolov9/onnx/camstack-yolov9s.onnx"),
|
|
463
|
+
sizeMB: 28
|
|
464
|
+
},
|
|
465
|
+
coreml: {
|
|
466
|
+
url: hf("objectDetection/yolov9/coreml/camstack-yolov9s.mlpackage"),
|
|
467
|
+
sizeMB: 14,
|
|
468
|
+
isDirectory: true,
|
|
469
|
+
files: [...MLPACKAGE_FILES],
|
|
470
|
+
runtimes: ["python"]
|
|
471
|
+
},
|
|
472
|
+
openvino: ovFormat(hf("objectDetection/yolov9/openvino/camstack-yolov9s.xml"), 16)
|
|
617
473
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
worker: this.opts.workerLabel,
|
|
644
|
-
reqId
|
|
645
|
-
} });
|
|
646
|
-
continue;
|
|
647
|
-
}
|
|
648
|
-
this.pending.delete(reqId);
|
|
649
|
-
try {
|
|
650
|
-
const parsed = JSON.parse(jsonBytes.toString("utf8"));
|
|
651
|
-
entry.resolve(parsed);
|
|
652
|
-
} catch (err) {
|
|
653
|
-
entry.reject(err instanceof Error ? err : new Error(String(err)));
|
|
654
|
-
}
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
id: "yolov9c",
|
|
477
|
+
legacy: true,
|
|
478
|
+
name: "YOLOv9 C",
|
|
479
|
+
description: "YOLOv9 C — high-accuracy compact model",
|
|
480
|
+
inputSize: {
|
|
481
|
+
width: 640,
|
|
482
|
+
height: 640
|
|
483
|
+
},
|
|
484
|
+
labels: [],
|
|
485
|
+
preprocessMode: "letterbox",
|
|
486
|
+
formats: {
|
|
487
|
+
onnx: {
|
|
488
|
+
url: hf("objectDetection/yolov9/onnx/camstack-yolov9c.onnx"),
|
|
489
|
+
sizeMB: 97
|
|
490
|
+
},
|
|
491
|
+
coreml: {
|
|
492
|
+
url: hf("objectDetection/yolov9/coreml/camstack-yolov9c.mlpackage"),
|
|
493
|
+
sizeMB: 48,
|
|
494
|
+
isDirectory: true,
|
|
495
|
+
files: [...MLPACKAGE_FILES],
|
|
496
|
+
runtimes: ["python"]
|
|
497
|
+
},
|
|
498
|
+
openvino: ovFormat(hf("objectDetection/yolov9/openvino/camstack-yolov9c.xml"), 49)
|
|
655
499
|
}
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
this.log = logger;
|
|
685
|
-
this.concurrency = Math.max(1, options.concurrency ?? 1);
|
|
686
|
-
this.tuning = options.tuning ?? null;
|
|
687
|
-
this.numWorkers = Math.max(1, options.numWorkers ?? 1);
|
|
688
|
-
}
|
|
689
|
-
/** Pid of the first worker (for legacy callers). Use `getPids()` for all. */
|
|
690
|
-
getPid() {
|
|
691
|
-
return this.workers[0]?.getPid() ?? null;
|
|
692
|
-
}
|
|
693
|
-
getPids() {
|
|
694
|
-
return this.workers.map((w) => w.getPid());
|
|
695
|
-
}
|
|
696
|
-
/**
|
|
697
|
-
* Start N pool workers in parallel. Each worker loads the same
|
|
698
|
-
* initial model set so subsequent inference calls land on a worker
|
|
699
|
-
* with the requested model already in its slot map.
|
|
700
|
-
*/
|
|
701
|
-
async initialize(initialModels = []) {
|
|
702
|
-
const scriptPath = this.resolveScriptPath();
|
|
703
|
-
for (let i = 0; i < this.numWorkers; i++) this.workers.push(new PoolWorker({
|
|
704
|
-
pythonPath: this.pythonPath,
|
|
705
|
-
scriptPath,
|
|
706
|
-
poolRuntime: this.poolRuntime,
|
|
707
|
-
concurrency: this.concurrency,
|
|
708
|
-
tuning: this.tuning,
|
|
709
|
-
logger: this.log,
|
|
710
|
-
workerLabel: `w${i}`
|
|
711
|
-
}));
|
|
712
|
-
const t0 = performance.now();
|
|
713
|
-
const results = await Promise.all(this.workers.map((w) => w.initialize(initialModels)));
|
|
714
|
-
const startupMs = Math.round(performance.now() - t0);
|
|
715
|
-
this.nextFreeIndex = initialModels.length;
|
|
716
|
-
const loadedCount = results[0]?.loadedCount ?? 0;
|
|
717
|
-
const innerWorkers = results.reduce((s, r) => s + r.workers, 0);
|
|
718
|
-
this.log.info("Pool ready", { meta: {
|
|
719
|
-
loadedCount,
|
|
720
|
-
startupMs,
|
|
721
|
-
runtime: this.poolRuntime,
|
|
722
|
-
workers: this.numWorkers,
|
|
723
|
-
pythonWorkers: innerWorkers,
|
|
724
|
-
pids: this.getPids()
|
|
725
|
-
} });
|
|
726
|
-
return {
|
|
727
|
-
startupMs,
|
|
728
|
-
loadedCount,
|
|
729
|
-
workers: this.numWorkers
|
|
730
|
-
};
|
|
731
|
-
}
|
|
732
|
-
async infer(modelIndex, jpeg) {
|
|
733
|
-
const w = this.pickWorker();
|
|
734
|
-
return this.trackDroppedResponse(await w.infer(this.encodeModelByte(modelIndex), jpeg), modelIndex);
|
|
735
|
-
}
|
|
736
|
-
async inferRaw(modelIndex, raw, width, height, format) {
|
|
737
|
-
const w = this.pickWorker();
|
|
738
|
-
return this.trackDroppedResponse(await w.inferRaw(this.encodeModelByte(modelIndex), raw, width, height, format), modelIndex);
|
|
739
|
-
}
|
|
740
|
-
async inferBatch(modelIndex, items) {
|
|
741
|
-
if (items.length > 255) throw new Error(`SharedInferencePool.inferBatch: max 255 items per call, got ${items.length}`);
|
|
742
|
-
return this.pickWorker().inferBatch(this.encodeModelByte(modelIndex), items);
|
|
743
|
-
}
|
|
744
|
-
async inferCached(modelIndex, frameId) {
|
|
745
|
-
const w = this.pickWorker();
|
|
746
|
-
return this.trackDroppedResponse(await w.inferCached(this.encodeModelByte(modelIndex), frameId), modelIndex);
|
|
747
|
-
}
|
|
748
|
-
/**
|
|
749
|
-
* Total `"dropped": true` responses seen from the Python pool — frames
|
|
750
|
-
* shed by the per-model in-flight bound under overload. Diagnostic
|
|
751
|
-
* counter (monotonic for the pool's lifetime).
|
|
752
|
-
*/
|
|
753
|
-
getDroppedResponseCount() {
|
|
754
|
-
return this.droppedResponseCount;
|
|
755
|
-
}
|
|
756
|
-
getHandle(modelIndex) {
|
|
757
|
-
return new PoolHandle(this, modelIndex);
|
|
758
|
-
}
|
|
759
|
-
async cacheFrame(raw, width, height, format) {
|
|
760
|
-
const frameId = this.nextFrameId++;
|
|
761
|
-
await Promise.all(this.workers.map((w) => w.cacheFrame(frameId, raw, width, height, format)));
|
|
762
|
-
return {
|
|
763
|
-
frameId,
|
|
764
|
-
width,
|
|
765
|
-
height
|
|
766
|
-
};
|
|
767
|
-
}
|
|
768
|
-
async uncacheFrame(frameId) {
|
|
769
|
-
await Promise.all(this.workers.map((w) => w.sendCommand({
|
|
770
|
-
cmd: "uncache_frame",
|
|
771
|
-
frameId
|
|
772
|
-
})));
|
|
773
|
-
}
|
|
774
|
-
async loadModel(index, config) {
|
|
775
|
-
const responses = await Promise.all(this.workers.map((w) => w.sendCommand({
|
|
776
|
-
cmd: "load",
|
|
777
|
-
index,
|
|
778
|
-
config: serializeModelConfig(config)
|
|
779
|
-
})));
|
|
780
|
-
for (const resp of responses) if (resp.status !== "ok") throw new Error(`Failed to load model at index ${index}: ${resp.error ?? "unknown"}`);
|
|
781
|
-
if (index >= this.nextFreeIndex) this.nextFreeIndex = index + 1;
|
|
782
|
-
return { loadMs: Math.max(...responses.map((r) => r.loadMs ?? 0)) };
|
|
783
|
-
}
|
|
784
|
-
async unloadModel(index) {
|
|
785
|
-
const responses = await Promise.all(this.workers.map((w) => w.sendCommand({
|
|
786
|
-
cmd: "unload",
|
|
787
|
-
index
|
|
788
|
-
})));
|
|
789
|
-
for (const resp of responses) if (resp.status !== "ok") throw new Error(`Failed to unload model at index ${index}: ${resp.error ?? "unknown"}`);
|
|
790
|
-
}
|
|
791
|
-
async replaceModel(index, config) {
|
|
792
|
-
const responses = await Promise.all(this.workers.map((w) => w.sendCommand({
|
|
793
|
-
cmd: "replace",
|
|
794
|
-
index,
|
|
795
|
-
config: serializeModelConfig(config)
|
|
796
|
-
})));
|
|
797
|
-
for (const resp of responses) if (resp.status !== "ok") throw new Error(`Failed to replace model at index ${index}: ${resp.error ?? "unknown"}`);
|
|
798
|
-
return { loadMs: Math.max(...responses.map((r) => r.loadMs ?? 0)) };
|
|
799
|
-
}
|
|
800
|
-
async getStatus() {
|
|
801
|
-
if (this.workers.length === 0) return [];
|
|
802
|
-
return (await this.workers[0].sendCommand({ cmd: "status" })).models ?? [];
|
|
803
|
-
}
|
|
804
|
-
allocateIndex() {
|
|
805
|
-
return this.nextFreeIndex++;
|
|
806
|
-
}
|
|
807
|
-
isReady() {
|
|
808
|
-
return this.workers.length > 0 && this.workers.every((w) => w.isReady());
|
|
809
|
-
}
|
|
810
|
-
async dispose() {
|
|
811
|
-
await Promise.all(this.workers.map((w) => w.dispose()));
|
|
812
|
-
this.workers.length = 0;
|
|
813
|
-
this.log.info("Pool process(es) terminated");
|
|
814
|
-
}
|
|
815
|
-
/**
|
|
816
|
-
* Recognise a Python overload-shed response (`"dropped": true`) on the
|
|
817
|
-
* single-frame inference paths. Previously indistinguishable from a
|
|
818
|
-
* genuine empty detection result — now counted + debug-logged so
|
|
819
|
-
* overload is measurable. The response passes through unchanged.
|
|
820
|
-
*/
|
|
821
|
-
trackDroppedResponse(result, modelIndex) {
|
|
822
|
-
if (result["dropped"] === true) {
|
|
823
|
-
this.droppedResponseCount++;
|
|
824
|
-
this.log.debug("Python pool shed frame under overload", { meta: {
|
|
825
|
-
modelIndex,
|
|
826
|
-
droppedTotal: this.droppedResponseCount
|
|
827
|
-
} });
|
|
828
|
-
}
|
|
829
|
-
return result;
|
|
830
|
-
}
|
|
831
|
-
pickWorker() {
|
|
832
|
-
if (this.workers.length === 0) throw new Error("SharedInferencePool: not initialized");
|
|
833
|
-
const idx = this.nextDispatchIdx;
|
|
834
|
-
this.nextDispatchIdx = (this.nextDispatchIdx + 1) % this.workers.length;
|
|
835
|
-
return this.workers[idx];
|
|
836
|
-
}
|
|
837
|
-
encodeModelByte(modelIndex) {
|
|
838
|
-
if (modelIndex < 0 || modelIndex > 254) throw new Error(`Model index ${modelIndex} out of range (0-254)`);
|
|
839
|
-
return modelIndex;
|
|
840
|
-
}
|
|
841
|
-
resolveScriptPath() {
|
|
842
|
-
const candidates = [
|
|
843
|
-
node_path.join(__dirname, "../../python/inference_pool.py"),
|
|
844
|
-
node_path.join(__dirname, "../python/inference_pool.py"),
|
|
845
|
-
node_path.join(__dirname, "../../../python/inference_pool.py")
|
|
846
|
-
];
|
|
847
|
-
try {
|
|
848
|
-
const pkgPath = require.resolve("@camstack/addon-detection-pipeline/package.json");
|
|
849
|
-
candidates.unshift(node_path.join(node_path.dirname(pkgPath), "python", "inference_pool.py"));
|
|
850
|
-
} catch {}
|
|
851
|
-
const found = candidates.find((p) => node_fs.existsSync(p));
|
|
852
|
-
if (!found) throw new Error(`inference_pool.py not found. Searched:\n${candidates.join("\n")}`);
|
|
853
|
-
return found;
|
|
854
|
-
}
|
|
855
|
-
};
|
|
856
|
-
function serializeModelConfig(config) {
|
|
857
|
-
const result = {
|
|
858
|
-
path: config.path,
|
|
859
|
-
inputSize: config.inputSize,
|
|
860
|
-
preprocessMode: config.preprocessMode,
|
|
861
|
-
postprocessor: config.postprocessor,
|
|
862
|
-
confidence: config.confidence
|
|
863
|
-
};
|
|
864
|
-
if (config.labels) result["labels"] = config.labels;
|
|
865
|
-
if (config.charset) result["charset"] = config.charset;
|
|
866
|
-
if (config.numClasses) result["numClasses"] = config.numClasses;
|
|
867
|
-
if (config.strides) result["strides"] = config.strides;
|
|
868
|
-
if (config.maskThreshold !== void 0) result["maskThreshold"] = config.maskThreshold;
|
|
869
|
-
if (config.device) result["device"] = config.device;
|
|
870
|
-
return result;
|
|
871
|
-
}
|
|
872
|
-
//#endregion
|
|
873
|
-
//#region src/detection-pipeline/engine/step-utils.ts
|
|
874
|
-
/**
|
|
875
|
-
* Flatten a step tree into a list of enabled video steps (DFS).
|
|
876
|
-
* Audio-classifier steps are excluded — they run independently
|
|
877
|
-
* via the audio pipeline, not the video inference pool.
|
|
878
|
-
*/
|
|
879
|
-
function flattenEnabledVideoSteps(steps) {
|
|
880
|
-
const result = [];
|
|
881
|
-
const walk = (nodes) => {
|
|
882
|
-
for (const step of nodes) {
|
|
883
|
-
if (!step.enabled) continue;
|
|
884
|
-
if (step.slot === "audio-classifier") continue;
|
|
885
|
-
result.push(step);
|
|
886
|
-
if (step.children?.length) walk(step.children);
|
|
887
|
-
}
|
|
888
|
-
};
|
|
889
|
-
walk(steps);
|
|
890
|
-
return result;
|
|
891
|
-
}
|
|
892
|
-
//#endregion
|
|
893
|
-
//#region src/detection-pipeline/engine/pipeline-model-manager.ts
|
|
894
|
-
var PipelineModelManager = class {
|
|
895
|
-
pool;
|
|
896
|
-
buildPoolConfig;
|
|
897
|
-
/** stepId → modelId → loaded entry. */
|
|
898
|
-
loaded = /* @__PURE__ */ new Map();
|
|
899
|
-
/** stepId → currently-active modelId (the runtime choice). */
|
|
900
|
-
activeByStep = /* @__PURE__ */ new Map();
|
|
901
|
-
/** Monotonic counter feeding the LRU tick on every touch. */
|
|
902
|
-
lruClock = 0;
|
|
903
|
-
log;
|
|
904
|
-
maxModelsPerStep;
|
|
905
|
-
constructor(pool, buildPoolConfig, logger, options) {
|
|
906
|
-
this.pool = pool;
|
|
907
|
-
this.buildPoolConfig = buildPoolConfig;
|
|
908
|
-
this.log = logger;
|
|
909
|
-
this.maxModelsPerStep = options?.maxModelsPerStep ?? 4;
|
|
910
|
-
}
|
|
911
|
-
/**
|
|
912
|
-
* Apply a new pipeline configuration — driven by the runtime config
|
|
913
|
-
* (camera-serving pipeline). Rewrites the *active* model per step,
|
|
914
|
-
* keeping prior actives warm in extra slots up to `maxModelsPerStep`.
|
|
915
|
-
*
|
|
916
|
-
* Steps that disappear from `newSteps` get fully unloaded (every
|
|
917
|
-
* model variant for that stepId).
|
|
918
|
-
*/
|
|
919
|
-
async applyConfig(newSteps) {
|
|
920
|
-
const enabledSteps = flattenEnabledVideoSteps(newSteps);
|
|
921
|
-
const diff = this.computeDiff(enabledSteps);
|
|
922
|
-
for (const removed of diff.removed) {
|
|
923
|
-
await this.unloadEntry(removed);
|
|
924
|
-
this.log.info("Unloading step", { meta: {
|
|
925
|
-
step: removed.stepId,
|
|
926
|
-
modelId: removed.modelId,
|
|
927
|
-
poolIndex: removed.poolIndex
|
|
928
|
-
} });
|
|
929
|
-
}
|
|
930
|
-
for (const added of diff.added) {
|
|
931
|
-
await this.ensureLoaded(added.addonId, added.modelId);
|
|
932
|
-
this.activeByStep.set(added.addonId, added.modelId);
|
|
933
|
-
this.log.info("Step loaded", { meta: {
|
|
934
|
-
step: added.addonId,
|
|
935
|
-
modelId: added.modelId,
|
|
936
|
-
active: true
|
|
937
|
-
} });
|
|
938
|
-
}
|
|
939
|
-
for (const { step, oldModelId } of diff.modelChanged) {
|
|
940
|
-
await this.ensureLoaded(step.addonId, step.modelId);
|
|
941
|
-
this.activeByStep.set(step.addonId, step.modelId);
|
|
942
|
-
this.log.info("Active model switched", { meta: {
|
|
943
|
-
step: step.addonId,
|
|
944
|
-
fromModelId: oldModelId,
|
|
945
|
-
toModelId: step.modelId
|
|
946
|
-
} });
|
|
947
|
-
await this.enforceLruCap(step.addonId);
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
/**
|
|
951
|
-
* Get an `IInferenceEngine` handle. Without `modelId`, returns the
|
|
952
|
-
* active model for the step (camera-runtime hot path). Pass
|
|
953
|
-
* `modelId` to grab a specific warm variant (benchmark path).
|
|
954
|
-
* Touches the LRU clock so the looked-up model survives eviction
|
|
955
|
-
* pressure from concurrent loads.
|
|
956
|
-
*/
|
|
957
|
-
getHandle(stepId, modelId) {
|
|
958
|
-
const entry = this.resolveOrThrow(stepId, modelId);
|
|
959
|
-
this.touch(entry);
|
|
960
|
-
return this.pool.getHandle(entry.poolIndex);
|
|
961
|
-
}
|
|
962
|
-
/** True iff the step has any model loaded. */
|
|
963
|
-
isLoaded(stepId) {
|
|
964
|
-
return this.loaded.has(stepId) && (this.loaded.get(stepId)?.size ?? 0) > 0;
|
|
965
|
-
}
|
|
966
|
-
/** True iff the specific (stepId, modelId) pair is resident. */
|
|
967
|
-
isLoadedWithModel(stepId, modelId) {
|
|
968
|
-
return this.loaded.get(stepId)?.has(modelId) ?? false;
|
|
969
|
-
}
|
|
970
|
-
/** ModelId currently designated as active for the step. */
|
|
971
|
-
getLoadedModelId(stepId) {
|
|
972
|
-
return this.activeByStep.get(stepId);
|
|
973
|
-
}
|
|
974
|
-
/**
|
|
975
|
-
* Pool index for a given (stepId, modelId). Without `modelId`,
|
|
976
|
-
* returns the active model's index. Used by the inference fast
|
|
977
|
-
* paths that bypass `getHandle` and call `pool.inferBatch` directly.
|
|
978
|
-
*/
|
|
979
|
-
getPoolIndex(stepId, modelId) {
|
|
980
|
-
const entry = this.resolve(stepId, modelId);
|
|
981
|
-
if (!entry) return null;
|
|
982
|
-
this.touch(entry);
|
|
983
|
-
return entry.poolIndex;
|
|
984
|
-
}
|
|
985
|
-
/**
|
|
986
|
-
* Direct access to the underlying pool — exposed so the provider can
|
|
987
|
-
* call `inferBatch` without re-fetching individual handles. Pool
|
|
988
|
-
* lifetime is owned by `EngineFactory`, so callers must not dispose.
|
|
989
|
-
*/
|
|
990
|
-
getPool() {
|
|
991
|
-
return this.pool;
|
|
992
|
-
}
|
|
993
|
-
/**
|
|
994
|
-
* Snapshot of every (stepId, modelId, poolIndex) currently resident.
|
|
995
|
-
* Used by the `Warm instances` UI tab + cap-level `listLoadedEngines`
|
|
996
|
-
* to surface both the runtime model and the warm extras.
|
|
997
|
-
*/
|
|
998
|
-
getLoadedSteps() {
|
|
999
|
-
const out = [];
|
|
1000
|
-
for (const [stepId, models] of this.loaded) {
|
|
1001
|
-
const activeModelId = this.activeByStep.get(stepId);
|
|
1002
|
-
for (const entry of models.values()) out.push({
|
|
1003
|
-
stepId: entry.stepId,
|
|
1004
|
-
modelId: entry.modelId,
|
|
1005
|
-
poolIndex: entry.poolIndex,
|
|
1006
|
-
active: entry.modelId === activeModelId
|
|
1007
|
-
});
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
id: "yolo26n",
|
|
503
|
+
group: {
|
|
504
|
+
family: "yolo26",
|
|
505
|
+
tier: "n"
|
|
506
|
+
},
|
|
507
|
+
name: "YOLO26 Nano",
|
|
508
|
+
description: "YOLO26 Nano — latest generation ultra-lightweight detector with attention mechanism",
|
|
509
|
+
inputSize: {
|
|
510
|
+
width: 640,
|
|
511
|
+
height: 640
|
|
512
|
+
},
|
|
513
|
+
labels: [],
|
|
514
|
+
preprocessMode: "letterbox",
|
|
515
|
+
formats: {
|
|
516
|
+
onnx: {
|
|
517
|
+
url: hf("objectDetection/yolo26/onnx/camstack-yolo26n.onnx"),
|
|
518
|
+
sizeMB: 10
|
|
519
|
+
},
|
|
520
|
+
coreml: {
|
|
521
|
+
url: hf("objectDetection/yolo26/coreml/camstack-yolo26n.mlpackage"),
|
|
522
|
+
sizeMB: 5,
|
|
523
|
+
isDirectory: true,
|
|
524
|
+
files: [...MLPACKAGE_FILES],
|
|
525
|
+
runtimes: ["python"]
|
|
526
|
+
},
|
|
527
|
+
openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26n.xml"), 9)
|
|
1008
528
|
}
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
id: "yolo26s",
|
|
532
|
+
group: {
|
|
533
|
+
family: "yolo26",
|
|
534
|
+
tier: "s"
|
|
535
|
+
},
|
|
536
|
+
name: "YOLO26 Small",
|
|
537
|
+
description: "YOLO26 Small — balanced speed/accuracy with attention-based architecture",
|
|
538
|
+
inputSize: {
|
|
539
|
+
width: 640,
|
|
540
|
+
height: 640
|
|
541
|
+
},
|
|
542
|
+
labels: [],
|
|
543
|
+
preprocessMode: "letterbox",
|
|
544
|
+
formats: {
|
|
545
|
+
onnx: {
|
|
546
|
+
url: hf("objectDetection/yolo26/onnx/camstack-yolo26s.onnx"),
|
|
547
|
+
sizeMB: 37
|
|
548
|
+
},
|
|
549
|
+
coreml: {
|
|
550
|
+
url: hf("objectDetection/yolo26/coreml/camstack-yolo26s.mlpackage"),
|
|
551
|
+
sizeMB: 18,
|
|
552
|
+
isDirectory: true,
|
|
553
|
+
files: [...MLPACKAGE_FILES],
|
|
554
|
+
runtimes: ["python"]
|
|
555
|
+
},
|
|
556
|
+
openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26s.xml"), 36)
|
|
1024
557
|
}
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
id: "yolo26m",
|
|
561
|
+
group: {
|
|
562
|
+
family: "yolo26",
|
|
563
|
+
tier: "m"
|
|
564
|
+
},
|
|
565
|
+
name: "YOLO26 Medium",
|
|
566
|
+
description: "YOLO26 Medium — high accuracy general-purpose detector",
|
|
567
|
+
inputSize: {
|
|
568
|
+
width: 640,
|
|
569
|
+
height: 640
|
|
570
|
+
},
|
|
571
|
+
labels: [],
|
|
572
|
+
preprocessMode: "letterbox",
|
|
573
|
+
formats: {
|
|
574
|
+
onnx: {
|
|
575
|
+
url: hf("objectDetection/yolo26/onnx/camstack-yolo26m.onnx"),
|
|
576
|
+
sizeMB: 78
|
|
577
|
+
},
|
|
578
|
+
coreml: {
|
|
579
|
+
url: hf("objectDetection/yolo26/coreml/camstack-yolo26m.mlpackage"),
|
|
580
|
+
sizeMB: 39,
|
|
581
|
+
isDirectory: true,
|
|
582
|
+
files: [...MLPACKAGE_FILES],
|
|
583
|
+
runtimes: ["python"]
|
|
584
|
+
},
|
|
585
|
+
openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26m.xml"), 78)
|
|
1036
586
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
id: "yolo26l",
|
|
590
|
+
group: {
|
|
591
|
+
family: "yolo26",
|
|
592
|
+
tier: "l"
|
|
593
|
+
},
|
|
594
|
+
name: "YOLO26 Large",
|
|
595
|
+
description: "YOLO26 Large — maximum accuracy for demanding deployments",
|
|
596
|
+
inputSize: {
|
|
597
|
+
width: 640,
|
|
598
|
+
height: 640
|
|
599
|
+
},
|
|
600
|
+
labels: [],
|
|
601
|
+
preprocessMode: "letterbox",
|
|
602
|
+
formats: {
|
|
603
|
+
onnx: {
|
|
604
|
+
url: hf("objectDetection/yolo26/onnx/camstack-yolo26l.onnx"),
|
|
605
|
+
sizeMB: 95
|
|
606
|
+
},
|
|
607
|
+
coreml: {
|
|
608
|
+
url: hf("objectDetection/yolo26/coreml/camstack-yolo26l.mlpackage"),
|
|
609
|
+
sizeMB: 48,
|
|
610
|
+
isDirectory: true,
|
|
611
|
+
files: [...MLPACKAGE_FILES],
|
|
612
|
+
runtimes: ["python"]
|
|
613
|
+
},
|
|
614
|
+
openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26l.xml"), 95)
|
|
1041
615
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
id: "yolo26x",
|
|
619
|
+
legacy: true,
|
|
620
|
+
name: "YOLO26 XLarge",
|
|
621
|
+
description: "YOLO26 XLarge — highest accuracy, attention-based architecture",
|
|
622
|
+
inputSize: {
|
|
623
|
+
width: 640,
|
|
624
|
+
height: 640
|
|
625
|
+
},
|
|
626
|
+
labels: [],
|
|
627
|
+
preprocessMode: "letterbox",
|
|
628
|
+
formats: {
|
|
629
|
+
onnx: {
|
|
630
|
+
url: hf("objectDetection/yolo26/onnx/camstack-yolo26x.onnx"),
|
|
631
|
+
sizeMB: 213
|
|
632
|
+
},
|
|
633
|
+
coreml: {
|
|
634
|
+
url: hf("objectDetection/yolo26/coreml/camstack-yolo26x.mlpackage"),
|
|
635
|
+
sizeMB: 107,
|
|
636
|
+
isDirectory: true,
|
|
637
|
+
files: [...MLPACKAGE_FILES],
|
|
638
|
+
runtimes: ["python"]
|
|
639
|
+
},
|
|
640
|
+
openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26x.xml"), 213)
|
|
1052
641
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
const perStep = this.loaded.get(entry.stepId);
|
|
1079
|
-
perStep?.delete(entry.modelId);
|
|
1080
|
-
if (perStep && perStep.size === 0) {
|
|
1081
|
-
this.loaded.delete(entry.stepId);
|
|
1082
|
-
this.activeByStep.delete(entry.stepId);
|
|
642
|
+
},
|
|
643
|
+
{
|
|
644
|
+
id: "scrypted-yolov9t-relu",
|
|
645
|
+
legacy: true,
|
|
646
|
+
name: "YOLOv9 Tiny ReLU",
|
|
647
|
+
description: "YOLOv9t ReLU — optimized for int8 quantization",
|
|
648
|
+
inputSize: {
|
|
649
|
+
width: 320,
|
|
650
|
+
height: 320
|
|
651
|
+
},
|
|
652
|
+
labels: [],
|
|
653
|
+
preprocessMode: "letterbox",
|
|
654
|
+
formats: {
|
|
655
|
+
onnx: {
|
|
656
|
+
url: hfScrypted("onnx/scrypted_yolov9t_relu/scrypted_yolov9t_relu.onnx"),
|
|
657
|
+
sizeMB: 8.1
|
|
658
|
+
},
|
|
659
|
+
coreml: {
|
|
660
|
+
url: hfScrypted("coreml/scrypted_yolov9t_relu/scrypted_yolov9t_relu.mlpackage"),
|
|
661
|
+
sizeMB: 4,
|
|
662
|
+
isDirectory: true,
|
|
663
|
+
files: [...MLPACKAGE_FILES],
|
|
664
|
+
runtimes: ["python"]
|
|
665
|
+
},
|
|
666
|
+
openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9t_relu.xml"), 6)
|
|
1083
667
|
}
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
id: "scrypted-yolov9s-relu",
|
|
671
|
+
legacy: true,
|
|
672
|
+
name: "YOLOv9 Small ReLU",
|
|
673
|
+
description: "YOLOv9s ReLU — improved efficiency, int8 ready",
|
|
674
|
+
inputSize: {
|
|
675
|
+
width: 320,
|
|
676
|
+
height: 320
|
|
677
|
+
},
|
|
678
|
+
labels: [],
|
|
679
|
+
preprocessMode: "letterbox",
|
|
680
|
+
formats: {
|
|
681
|
+
onnx: {
|
|
682
|
+
url: hfScrypted("onnx/scrypted_yolov9s_relu/scrypted_yolov9s_relu.onnx"),
|
|
683
|
+
sizeMB: 27.9
|
|
684
|
+
},
|
|
685
|
+
coreml: {
|
|
686
|
+
url: hfScrypted("coreml/scrypted_yolov9s_relu/scrypted_yolov9s_relu.mlpackage"),
|
|
687
|
+
sizeMB: 14,
|
|
688
|
+
isDirectory: true,
|
|
689
|
+
files: [...MLPACKAGE_FILES],
|
|
690
|
+
runtimes: ["python"]
|
|
691
|
+
},
|
|
692
|
+
openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9s_relu.xml"), 16)
|
|
1094
693
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
id: "scrypted-yolov9c-relu",
|
|
697
|
+
legacy: true,
|
|
698
|
+
name: "YOLOv9 C ReLU",
|
|
699
|
+
description: "YOLOv9c ReLU — high-accuracy, int8 ready",
|
|
700
|
+
inputSize: {
|
|
701
|
+
width: 320,
|
|
702
|
+
height: 320
|
|
703
|
+
},
|
|
704
|
+
labels: [],
|
|
705
|
+
preprocessMode: "letterbox",
|
|
706
|
+
formats: {
|
|
707
|
+
onnx: {
|
|
708
|
+
url: hfScrypted("onnx/scrypted_yolov9c_relu/scrypted_yolov9c_relu.onnx"),
|
|
709
|
+
sizeMB: 96.9
|
|
710
|
+
},
|
|
711
|
+
coreml: {
|
|
712
|
+
url: hfScrypted("coreml/scrypted_yolov9c_relu/scrypted_yolov9c_relu.mlpackage"),
|
|
713
|
+
sizeMB: 48,
|
|
714
|
+
isDirectory: true,
|
|
715
|
+
files: [...MLPACKAGE_FILES],
|
|
716
|
+
runtimes: ["python"]
|
|
717
|
+
},
|
|
718
|
+
openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9c_relu.xml"), 49)
|
|
1105
719
|
}
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
id: "scrypted-yolov9m-relu",
|
|
723
|
+
legacy: true,
|
|
724
|
+
name: "YOLOv9 M ReLU",
|
|
725
|
+
description: "YOLOv9m ReLU — medium, int8 ready",
|
|
726
|
+
inputSize: {
|
|
727
|
+
width: 320,
|
|
728
|
+
height: 320
|
|
729
|
+
},
|
|
730
|
+
labels: [],
|
|
731
|
+
preprocessMode: "letterbox",
|
|
732
|
+
formats: {
|
|
733
|
+
onnx: {
|
|
734
|
+
url: hfScrypted("onnx/scrypted_yolov9m_relu/scrypted_yolov9m_relu.onnx"),
|
|
735
|
+
sizeMB: 76.6
|
|
736
|
+
},
|
|
737
|
+
coreml: {
|
|
738
|
+
url: hfScrypted("coreml/scrypted_yolov9m_relu/scrypted_yolov9m_relu.mlpackage"),
|
|
739
|
+
sizeMB: 38,
|
|
740
|
+
isDirectory: true,
|
|
741
|
+
files: [...MLPACKAGE_FILES],
|
|
742
|
+
runtimes: ["python"]
|
|
743
|
+
},
|
|
744
|
+
openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9m_relu.xml"), 38)
|
|
745
|
+
}
|
|
746
|
+
},
|
|
747
|
+
ovPrecisionVariant("yolov9t", "objectDetection/yolov9/openvino", "YOLOv9 Tiny", "fp16", 5, true),
|
|
748
|
+
ovPrecisionVariant("yolov9t", "objectDetection/yolov9/openvino", "YOLOv9 Tiny", "int8", 3, true),
|
|
749
|
+
ovPrecisionVariant("yolov9s", "objectDetection/yolov9/openvino", "YOLOv9 Small", "fp16", 15, true),
|
|
750
|
+
ovPrecisionVariant("yolov9s", "objectDetection/yolov9/openvino", "YOLOv9 Small", "int8", 8, true),
|
|
751
|
+
ovPrecisionVariant("yolo26n", "objectDetection/yolo26/openvino", "YOLO26 Nano", "fp16", 5, true),
|
|
752
|
+
ovPrecisionVariant("yolo26n", "objectDetection/yolo26/openvino", "YOLO26 Nano", "int8", 3),
|
|
753
|
+
ovPrecisionVariant("yolo26s", "objectDetection/yolo26/openvino", "YOLO26 Small", "fp16", 19, true),
|
|
754
|
+
ovPrecisionVariant("yolo26s", "objectDetection/yolo26/openvino", "YOLO26 Small", "int8", 10),
|
|
755
|
+
ovPrecisionVariant("yolo26m", "objectDetection/yolo26/openvino", "YOLO26 Medium", "fp16", 41, true),
|
|
756
|
+
ovPrecisionVariant("yolo26m", "objectDetection/yolo26/openvino", "YOLO26 Medium", "int8", 21),
|
|
757
|
+
ovPrecisionVariant("yolo26l", "objectDetection/yolo26/openvino", "YOLO26 Large", "fp16", 50, true),
|
|
758
|
+
ovPrecisionVariant("yolo26l", "objectDetection/yolo26/openvino", "YOLO26 Large", "int8", 25),
|
|
759
|
+
...yolo26Reduced("n", 320, {
|
|
760
|
+
onnx: 10.47,
|
|
761
|
+
coreml: 5.05,
|
|
762
|
+
ovFp32: 10.05,
|
|
763
|
+
ovInt8: 3.29
|
|
764
|
+
}),
|
|
765
|
+
...yolo26Reduced("n", 256, {
|
|
766
|
+
onnx: 10.47,
|
|
767
|
+
coreml: 5.05,
|
|
768
|
+
ovFp32: 10.05,
|
|
769
|
+
ovInt8: 3.29
|
|
770
|
+
}),
|
|
771
|
+
...yolo26Reduced("s", 320, {
|
|
772
|
+
onnx: 38.82,
|
|
773
|
+
coreml: 19.23,
|
|
774
|
+
ovFp32: 38.4,
|
|
775
|
+
ovInt8: 10.43
|
|
776
|
+
}),
|
|
777
|
+
...yolo26Reduced("s", 256, {
|
|
778
|
+
onnx: 38.82,
|
|
779
|
+
coreml: 19.23,
|
|
780
|
+
ovFp32: 38.4,
|
|
781
|
+
ovInt8: 10.43
|
|
782
|
+
}),
|
|
783
|
+
...yolo26Reduced("m", 320, {
|
|
784
|
+
onnx: 83.31,
|
|
785
|
+
coreml: 41.07,
|
|
786
|
+
ovFp32: 82.09,
|
|
787
|
+
ovInt8: 21.47
|
|
788
|
+
}),
|
|
789
|
+
...yolo26Reduced("m", 256, {
|
|
790
|
+
onnx: 83.31,
|
|
791
|
+
coreml: 41.07,
|
|
792
|
+
ovFp32: 82.09,
|
|
793
|
+
ovInt8: 21.47
|
|
794
|
+
}),
|
|
795
|
+
...yolo26Reduced("l", 320, {
|
|
796
|
+
onnx: 101.74,
|
|
797
|
+
coreml: 49.97,
|
|
798
|
+
ovFp32: 99.85,
|
|
799
|
+
ovInt8: 26.31
|
|
800
|
+
}),
|
|
801
|
+
...yolo26Reduced("l", 256, {
|
|
802
|
+
onnx: 101.74,
|
|
803
|
+
coreml: 49.97,
|
|
804
|
+
ovFp32: 99.85,
|
|
805
|
+
ovInt8: 26.31
|
|
806
|
+
}),
|
|
807
|
+
ovPrecisionVariant("yolo26x", "objectDetection/yolo26/openvino", "YOLO26 XLarge", "fp16", 112, true),
|
|
808
|
+
ovPrecisionVariant("yolo26x", "objectDetection/yolo26/openvino", "YOLO26 XLarge", "int8", 56, true)
|
|
809
|
+
];
|
|
810
|
+
var FACE_DETECTION_MODELS = [{
|
|
811
|
+
id: "scrfd-2.5g",
|
|
812
|
+
name: "SCRFD 2.5G",
|
|
813
|
+
description: "SCRFD 2.5G — balanced face detection model",
|
|
814
|
+
inputSize: {
|
|
815
|
+
width: 640,
|
|
816
|
+
height: 640
|
|
817
|
+
},
|
|
818
|
+
labels: [{
|
|
819
|
+
id: "face",
|
|
820
|
+
name: "Face"
|
|
821
|
+
}],
|
|
822
|
+
preprocessMode: "letterbox",
|
|
823
|
+
formats: {
|
|
824
|
+
onnx: {
|
|
825
|
+
url: hf("faceDetection/scrfd/onnx/camstack-scrfd-2.5g.onnx"),
|
|
826
|
+
sizeMB: 3.1
|
|
827
|
+
},
|
|
828
|
+
coreml: {
|
|
829
|
+
url: hf("faceDetection/scrfd/coreml/camstack-scrfd-2.5g.mlpackage"),
|
|
830
|
+
sizeMB: 1.7,
|
|
831
|
+
isDirectory: true,
|
|
832
|
+
files: [...MLPACKAGE_FILES],
|
|
833
|
+
runtimes: ["python"]
|
|
834
|
+
},
|
|
835
|
+
openvino: ovFormat(hf("faceDetection/scrfd/openvino/camstack-scrfd-2.5g.xml"), 1.8)
|
|
1106
836
|
}
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
837
|
+
}, {
|
|
838
|
+
id: "scrypted-yolov9t-face",
|
|
839
|
+
name: "YOLOv9t Face ReLU",
|
|
840
|
+
description: "YOLOv9t face detection — YOLO-based, fast, 320x320 ReLU",
|
|
841
|
+
inputSize: {
|
|
842
|
+
width: 320,
|
|
843
|
+
height: 320
|
|
844
|
+
},
|
|
845
|
+
labels: [{
|
|
846
|
+
id: "face",
|
|
847
|
+
name: "Face"
|
|
848
|
+
}],
|
|
849
|
+
preprocessMode: "letterbox",
|
|
850
|
+
formats: {
|
|
851
|
+
onnx: {
|
|
852
|
+
url: hfScrypted("onnx/scrypted_yolov9t_relu_face/scrypted_yolov9t_relu_face.onnx"),
|
|
853
|
+
sizeMB: 8.1
|
|
854
|
+
},
|
|
855
|
+
coreml: {
|
|
856
|
+
url: hfScrypted("coreml/scrypted_yolov9t_relu_face/scrypted_yolov9t_relu_face.mlpackage"),
|
|
857
|
+
sizeMB: 4,
|
|
858
|
+
isDirectory: true,
|
|
859
|
+
files: [...MLPACKAGE_FILES],
|
|
860
|
+
runtimes: ["python"]
|
|
861
|
+
},
|
|
862
|
+
openvino: ovFormat(hf("faceDetection/scrypted-yolov9-face/openvino/scrypted_yolov9t_relu_face.xml"), 6)
|
|
1118
863
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
864
|
+
}];
|
|
865
|
+
var FACE_EMBEDDING_MODELS = [{
|
|
866
|
+
id: "arcface-r100",
|
|
867
|
+
name: "ArcFace R100",
|
|
868
|
+
description: "ArcFace ResNet-100 — high-accuracy face recognition embeddings (512-d)",
|
|
869
|
+
inputSize: {
|
|
870
|
+
width: 112,
|
|
871
|
+
height: 112
|
|
872
|
+
},
|
|
873
|
+
inputLayout: "nhwc",
|
|
874
|
+
labels: [{
|
|
875
|
+
id: "embedding",
|
|
876
|
+
name: "Face Embedding"
|
|
877
|
+
}],
|
|
878
|
+
preprocessMode: "resize",
|
|
879
|
+
faceAlignment: true,
|
|
880
|
+
formats: {
|
|
881
|
+
onnx: {
|
|
882
|
+
url: hf("faceRecognition/arcface/onnx/camstack-arcface-arcface.onnx"),
|
|
883
|
+
sizeMB: 130
|
|
884
|
+
},
|
|
885
|
+
coreml: {
|
|
886
|
+
url: hf("faceRecognition/arcface/coreml/camstack-arcface-r100.mlpackage"),
|
|
887
|
+
sizeMB: 65,
|
|
888
|
+
isDirectory: true,
|
|
889
|
+
files: [...MLPACKAGE_FILES],
|
|
890
|
+
runtimes: ["python"]
|
|
891
|
+
},
|
|
892
|
+
openvino: ovFormat(hf("faceRecognition/arcface/openvino/camstack-arcface-r100.xml"), 65)
|
|
1121
893
|
}
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
unchanged
|
|
1149
|
-
};
|
|
894
|
+
}, {
|
|
895
|
+
id: "inception-resnet-v1",
|
|
896
|
+
name: "Inception ResNet V1",
|
|
897
|
+
description: "FaceNet-style face recognition embeddings (512-d) — hosted on plugin-models HF repo",
|
|
898
|
+
inputSize: {
|
|
899
|
+
width: 160,
|
|
900
|
+
height: 160
|
|
901
|
+
},
|
|
902
|
+
labels: [{
|
|
903
|
+
id: "embedding",
|
|
904
|
+
name: "Face Embedding"
|
|
905
|
+
}],
|
|
906
|
+
preprocessMode: "resize",
|
|
907
|
+
formats: {
|
|
908
|
+
onnx: {
|
|
909
|
+
url: hfScrypted("onnx/inception_resnet_v1/inception_resnet_v1.onnx"),
|
|
910
|
+
sizeMB: 89.6
|
|
911
|
+
},
|
|
912
|
+
coreml: {
|
|
913
|
+
url: hfScrypted("coreml/inception_resnet_v1/inception_resnet_v1.mlpackage"),
|
|
914
|
+
sizeMB: 45,
|
|
915
|
+
isDirectory: true,
|
|
916
|
+
files: [...MLPACKAGE_FILES],
|
|
917
|
+
runtimes: ["python"]
|
|
918
|
+
},
|
|
919
|
+
openvino: ovFormat(hf("faceRecognition/inception-resnet-v1/openvino/camstack-inception-resnet-v1.xml"), 45)
|
|
1150
920
|
}
|
|
1151
|
-
};
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
var hf = (path) => require_dist.hfModelUrl(HF_REPO, path);
|
|
1157
|
-
var hfScrypted = (path) => require_dist.hfModelUrl(HF_SCRYPTED, path);
|
|
1158
|
-
/**
|
|
1159
|
-
* Build an OpenVINO format entry (always python runtime).
|
|
1160
|
-
*
|
|
1161
|
-
* OpenVINO IR is a two-file bundle: a `.xml` topology + a sibling `.bin`
|
|
1162
|
-
* weights file with the same basename. We declare the `.bin` in `files` so
|
|
1163
|
-
* the (format-agnostic) downloader fetches it alongside the `.xml` — without
|
|
1164
|
-
* the weights, OpenVINO compile fails with "Empty weights data in bin file".
|
|
1165
|
-
* A plain `.onnx` run through the OpenVINO runtime (e.g. yamnet) has no
|
|
1166
|
-
* sibling, so none is added.
|
|
1167
|
-
*/
|
|
1168
|
-
var ovFormat = (url, sizeMB) => {
|
|
1169
|
-
const base = url.split("/").pop() ?? "";
|
|
1170
|
-
const files = base.endsWith(".xml") ? [base.replace(/\.xml$/, ".bin")] : void 0;
|
|
1171
|
-
return {
|
|
1172
|
-
url,
|
|
1173
|
-
sizeMB,
|
|
1174
|
-
runtimes: ["python"],
|
|
1175
|
-
...files ? { files } : {}
|
|
1176
|
-
};
|
|
1177
|
-
};
|
|
1178
|
-
/**
|
|
1179
|
-
* Build a precision-variant catalog entry (OpenVINO-only) derived from a base
|
|
1180
|
-
* detection model. fp16 halves the weights (Intel iGPU/NPU sweet spot); int8 is
|
|
1181
|
-
* NNCF post-training-quantized (~4× smaller, fastest on CPU/iGPU at a small
|
|
1182
|
-
* accuracy cost). The IRs live next to the base `.xml` on HF as
|
|
1183
|
-
* `camstack-<id>-<precision>.xml`. Lets an operator scale the model to the node
|
|
1184
|
-
* (e.g. yolo26x-int8 on a 265K, yolo26n-int8 on an N100).
|
|
1185
|
-
*/
|
|
1186
|
-
var ovPrecisionVariant = (baseId, ovDir, baseName, precision, sizeMB, legacy = false) => ({
|
|
1187
|
-
id: `${baseId}-${precision}`,
|
|
1188
|
-
name: `${baseName} (${precision.toUpperCase()})`,
|
|
1189
|
-
description: `${baseName} — OpenVINO ${precision.toUpperCase()} variant for Intel iGPU/NPU; scale by hardware`,
|
|
921
|
+
}];
|
|
922
|
+
var PLATE_DETECTION_MODELS = [{
|
|
923
|
+
id: "yolov8n-plate",
|
|
924
|
+
name: "YOLOv8 Nano — License Plate",
|
|
925
|
+
description: "YOLOv8 Nano fine-tuned for license plate detection",
|
|
1190
926
|
inputSize: {
|
|
1191
927
|
width: 640,
|
|
1192
928
|
height: 640
|
|
1193
929
|
},
|
|
1194
|
-
labels: [
|
|
930
|
+
labels: [{
|
|
931
|
+
id: "plate",
|
|
932
|
+
name: "License Plate"
|
|
933
|
+
}],
|
|
1195
934
|
preprocessMode: "letterbox",
|
|
1196
|
-
formats: {
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
tier: baseId.slice(-1),
|
|
1201
|
-
precision: "int8"
|
|
1202
|
-
} } : {}
|
|
1203
|
-
});
|
|
1204
|
-
var MLPACKAGE_FILES = [
|
|
1205
|
-
"Manifest.json",
|
|
1206
|
-
"Data/com.apple.CoreML/model.mlmodel",
|
|
1207
|
-
"Data/com.apple.CoreML/weights/weight.bin"
|
|
1208
|
-
];
|
|
1209
|
-
var OBJECT_DETECTION_MODELS = [
|
|
1210
|
-
{
|
|
1211
|
-
id: "yolov9t",
|
|
1212
|
-
legacy: true,
|
|
1213
|
-
name: "YOLOv9 Tiny",
|
|
1214
|
-
description: "YOLOv9 Tiny — ultra-lightweight next-gen detector",
|
|
1215
|
-
inputSize: {
|
|
1216
|
-
width: 640,
|
|
1217
|
-
height: 640
|
|
935
|
+
formats: {
|
|
936
|
+
onnx: {
|
|
937
|
+
url: hf("plateDetection/yolov8-plate/onnx/camstack-yolov8n-plate.onnx"),
|
|
938
|
+
sizeMB: 12
|
|
1218
939
|
},
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
940
|
+
coreml: {
|
|
941
|
+
url: hf("plateDetection/yolov8-plate/coreml/camstack-yolov8n-plate.mlpackage"),
|
|
942
|
+
sizeMB: 5.9,
|
|
943
|
+
isDirectory: true,
|
|
944
|
+
files: [...MLPACKAGE_FILES],
|
|
945
|
+
runtimes: ["python"]
|
|
946
|
+
},
|
|
947
|
+
openvino: ovFormat(hf("plateDetection/yolov8-plate/openvino/camstack-yolov8n-plate.xml"), 6.1)
|
|
948
|
+
}
|
|
949
|
+
}];
|
|
950
|
+
var PLATE_OCR_MODELS = [{
|
|
951
|
+
id: "vgg-english-g2",
|
|
952
|
+
name: "VGG English G2",
|
|
953
|
+
description: "EasyOCR VGG English G2 — text recognition for license plates and scene text",
|
|
954
|
+
inputSize: {
|
|
955
|
+
width: 384,
|
|
956
|
+
height: 64
|
|
1235
957
|
},
|
|
1236
|
-
{
|
|
1237
|
-
id: "
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
958
|
+
labels: [{
|
|
959
|
+
id: "text",
|
|
960
|
+
name: "Plate Text"
|
|
961
|
+
}],
|
|
962
|
+
preprocessMode: "resize",
|
|
963
|
+
formats: {
|
|
964
|
+
onnx: {
|
|
965
|
+
url: hfScrypted("onnx/vgg_english_g2/vgg_english_g2.onnx"),
|
|
966
|
+
sizeMB: 14.4
|
|
1244
967
|
},
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
968
|
+
coreml: {
|
|
969
|
+
url: hfScrypted("coreml/vgg_english_g2/vgg_english_g2.mlpackage"),
|
|
970
|
+
sizeMB: 11.2,
|
|
971
|
+
isDirectory: true,
|
|
972
|
+
files: [...MLPACKAGE_FILES],
|
|
973
|
+
runtimes: ["python"]
|
|
974
|
+
},
|
|
975
|
+
openvino: ovFormat(hf("plateRecognition/vgg_english_g2/openvino/vgg_english_g2.xml"), 7.2)
|
|
976
|
+
}
|
|
977
|
+
}];
|
|
978
|
+
var ANIMAL_CLASSIFIER_MODELS = [{
|
|
979
|
+
id: "animals-10",
|
|
980
|
+
name: "Animal Classifier (10)",
|
|
981
|
+
description: "ViT-based animal type classifier — cat, cow, dog, dolphin, eagle, panda, horse, monkey, sheep, spider",
|
|
982
|
+
inputSize: {
|
|
983
|
+
width: 224,
|
|
984
|
+
height: 224
|
|
1261
985
|
},
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
name: "
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
986
|
+
inputNormalization: "imagenet",
|
|
987
|
+
labels: [{
|
|
988
|
+
id: "animal-type",
|
|
989
|
+
name: "Animal Type"
|
|
990
|
+
}],
|
|
991
|
+
preprocessMode: "resize",
|
|
992
|
+
formats: {
|
|
993
|
+
onnx: {
|
|
994
|
+
url: hf("animalClassification/animals-10/onnx/camstack-animals-10.onnx"),
|
|
995
|
+
sizeMB: 328
|
|
1270
996
|
},
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
997
|
+
coreml: {
|
|
998
|
+
url: hf("animalClassification/animals-10/coreml/camstack-animals-10.mlpackage"),
|
|
999
|
+
sizeMB: 164,
|
|
1000
|
+
isDirectory: true,
|
|
1001
|
+
files: [...MLPACKAGE_FILES],
|
|
1002
|
+
runtimes: ["python"]
|
|
1003
|
+
},
|
|
1004
|
+
openvino: ovFormat(hf("animalClassification/animals-10/openvino/camstack-animals-10.xml"), 164)
|
|
1005
|
+
}
|
|
1006
|
+
}];
|
|
1007
|
+
var BIRD_CLASSIFIER_MODELS = [{
|
|
1008
|
+
id: "bird-nabirds-404",
|
|
1009
|
+
name: "NABirds (404 species)",
|
|
1010
|
+
description: "ResNet50 trained on NABirds — 404 North American species",
|
|
1011
|
+
inputSize: {
|
|
1012
|
+
width: 224,
|
|
1013
|
+
height: 224
|
|
1287
1014
|
},
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1015
|
+
inputNormalization: "imagenet",
|
|
1016
|
+
labels: [{
|
|
1017
|
+
id: "species",
|
|
1018
|
+
name: "Bird Species"
|
|
1019
|
+
}],
|
|
1020
|
+
preprocessMode: "resize",
|
|
1021
|
+
formats: {
|
|
1022
|
+
onnx: {
|
|
1023
|
+
url: hf("animalClassification/bird-nabirds/onnx/camstack-bird-nabirds-404.onnx"),
|
|
1024
|
+
sizeMB: 93
|
|
1025
|
+
},
|
|
1026
|
+
coreml: {
|
|
1027
|
+
url: hf("animalClassification/bird-nabirds/coreml/camstack-bird-nabirds-404.mlpackage"),
|
|
1028
|
+
sizeMB: 47,
|
|
1029
|
+
isDirectory: true,
|
|
1030
|
+
files: [...MLPACKAGE_FILES],
|
|
1031
|
+
runtimes: ["python"]
|
|
1032
|
+
},
|
|
1033
|
+
openvino: ovFormat(hf("animalClassification/bird-nabirds/openvino/camstack-bird-nabirds-404.xml"), 47)
|
|
1034
|
+
},
|
|
1035
|
+
extraFiles: [{
|
|
1036
|
+
url: hf("animalClassification/bird-nabirds/onnx/camstack-bird-nabirds-404-labels.json"),
|
|
1037
|
+
filename: "camstack-bird-nabirds-404-labels.json",
|
|
1038
|
+
sizeMB: .02
|
|
1039
|
+
}]
|
|
1040
|
+
}];
|
|
1041
|
+
var VEHICLE_CLASSIFIER_MODELS = [{
|
|
1042
|
+
id: "vehicle-type-efficientnet",
|
|
1043
|
+
name: "Vehicle Type (EfficientNet)",
|
|
1044
|
+
description: "EfficientNet-B4 vehicle make/model/year classifier — 8,949 classes from VMMRdb",
|
|
1045
|
+
inputSize: {
|
|
1046
|
+
width: 380,
|
|
1047
|
+
height: 380
|
|
1048
|
+
},
|
|
1049
|
+
inputNormalization: "imagenet",
|
|
1050
|
+
labels: [{
|
|
1051
|
+
id: "vehicle-type",
|
|
1052
|
+
name: "Vehicle Type"
|
|
1053
|
+
}],
|
|
1054
|
+
preprocessMode: "resize",
|
|
1055
|
+
formats: {
|
|
1056
|
+
onnx: {
|
|
1057
|
+
url: hf("vehicleClassification/efficientnet/onnx/camstack-vehicle-type-efficientnet.onnx"),
|
|
1058
|
+
sizeMB: 135
|
|
1293
1059
|
},
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1060
|
+
coreml: {
|
|
1061
|
+
url: hf("vehicleClassification/efficientnet/coreml/camstack-vehicle-type-efficientnet.mlpackage"),
|
|
1062
|
+
sizeMB: 10,
|
|
1063
|
+
isDirectory: true,
|
|
1064
|
+
files: [...MLPACKAGE_FILES],
|
|
1065
|
+
runtimes: ["python"]
|
|
1299
1066
|
},
|
|
1300
|
-
|
|
1301
|
-
preprocessMode: "letterbox",
|
|
1302
|
-
formats: {
|
|
1303
|
-
onnx: {
|
|
1304
|
-
url: hf("objectDetection/yolo26/onnx/camstack-yolo26n.onnx"),
|
|
1305
|
-
sizeMB: 10
|
|
1306
|
-
},
|
|
1307
|
-
coreml: {
|
|
1308
|
-
url: hf("objectDetection/yolo26/coreml/camstack-yolo26n.mlpackage"),
|
|
1309
|
-
sizeMB: 5,
|
|
1310
|
-
isDirectory: true,
|
|
1311
|
-
files: [...MLPACKAGE_FILES],
|
|
1312
|
-
runtimes: ["python"]
|
|
1313
|
-
},
|
|
1314
|
-
openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26n.xml"), 9)
|
|
1315
|
-
}
|
|
1067
|
+
openvino: ovFormat(hf("vehicleClassification/efficientnet/openvino/camstack-vehicle-type-efficientnet.xml"), 68)
|
|
1316
1068
|
},
|
|
1317
|
-
{
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1069
|
+
extraFiles: [{
|
|
1070
|
+
url: hf("vehicleClassification/efficientnet/camstack-vehicle-type-labels.json"),
|
|
1071
|
+
filename: "camstack-vehicle-type-labels.json",
|
|
1072
|
+
sizeMB: .2
|
|
1073
|
+
}]
|
|
1074
|
+
}];
|
|
1075
|
+
var SEGMENTATION_REFINER_MODELS = [{
|
|
1076
|
+
id: "u2netp",
|
|
1077
|
+
name: "U2-Net Portable",
|
|
1078
|
+
description: "U2-Net-P — ultra-lightweight salient object segmentation (4.7 MB)",
|
|
1079
|
+
inputSize: {
|
|
1080
|
+
width: 320,
|
|
1081
|
+
height: 320
|
|
1082
|
+
},
|
|
1083
|
+
labels: [{
|
|
1084
|
+
id: "mask",
|
|
1085
|
+
name: "Segmentation Mask"
|
|
1086
|
+
}],
|
|
1087
|
+
preprocessMode: "resize",
|
|
1088
|
+
formats: {
|
|
1089
|
+
onnx: {
|
|
1090
|
+
url: hf("segmentationRefiner/u2netp/onnx/camstack-u2netp.onnx"),
|
|
1091
|
+
sizeMB: 5
|
|
1322
1092
|
},
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1093
|
+
coreml: {
|
|
1094
|
+
url: hf("segmentationRefiner/u2netp/coreml/camstack-u2netp.mlpackage"),
|
|
1095
|
+
sizeMB: 3,
|
|
1096
|
+
isDirectory: true,
|
|
1097
|
+
files: [...MLPACKAGE_FILES],
|
|
1098
|
+
runtimes: ["python"]
|
|
1328
1099
|
},
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
url: hf("objectDetection/yolo26/onnx/camstack-yolo26s.onnx"),
|
|
1334
|
-
sizeMB: 37
|
|
1335
|
-
},
|
|
1336
|
-
coreml: {
|
|
1337
|
-
url: hf("objectDetection/yolo26/coreml/camstack-yolo26s.mlpackage"),
|
|
1338
|
-
sizeMB: 18,
|
|
1339
|
-
isDirectory: true,
|
|
1340
|
-
files: [...MLPACKAGE_FILES],
|
|
1341
|
-
runtimes: ["python"]
|
|
1342
|
-
},
|
|
1343
|
-
openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26s.xml"), 36)
|
|
1344
|
-
}
|
|
1345
|
-
},
|
|
1100
|
+
openvino: ovFormat(hf("segmentationRefiner/u2netp/openvino/camstack-u2netp.xml"), 2.5)
|
|
1101
|
+
}
|
|
1102
|
+
}];
|
|
1103
|
+
var INSTANCE_SEGMENTATION_MODELS = [
|
|
1346
1104
|
{
|
|
1347
|
-
id: "
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
tier: "m"
|
|
1351
|
-
},
|
|
1352
|
-
name: "YOLO26 Medium",
|
|
1353
|
-
description: "YOLO26 Medium — high accuracy general-purpose detector",
|
|
1105
|
+
id: "yolo26n-seg",
|
|
1106
|
+
name: "YOLO26 Nano Seg",
|
|
1107
|
+
description: "YOLO26 Nano Segmentation — ultra-lightweight instance segmentation with masks",
|
|
1354
1108
|
inputSize: {
|
|
1355
1109
|
width: 640,
|
|
1356
1110
|
height: 640
|
|
@@ -1359,27 +1113,23 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1359
1113
|
preprocessMode: "letterbox",
|
|
1360
1114
|
formats: {
|
|
1361
1115
|
onnx: {
|
|
1362
|
-
url: hf("
|
|
1363
|
-
sizeMB:
|
|
1116
|
+
url: hf("segmentation/yolo26-seg/onnx/camstack-yolo26n-seg.onnx"),
|
|
1117
|
+
sizeMB: 11
|
|
1364
1118
|
},
|
|
1365
1119
|
coreml: {
|
|
1366
|
-
url: hf("
|
|
1367
|
-
sizeMB:
|
|
1120
|
+
url: hf("segmentation/yolo26-seg/coreml/camstack-yolo26n-seg.mlpackage"),
|
|
1121
|
+
sizeMB: 6,
|
|
1368
1122
|
isDirectory: true,
|
|
1369
1123
|
files: [...MLPACKAGE_FILES],
|
|
1370
1124
|
runtimes: ["python"]
|
|
1371
1125
|
},
|
|
1372
|
-
openvino: ovFormat(hf("
|
|
1126
|
+
openvino: ovFormat(hf("segmentation/yolo26-seg/openvino/camstack-yolo26n-seg.xml"), 11)
|
|
1373
1127
|
}
|
|
1374
1128
|
},
|
|
1375
1129
|
{
|
|
1376
|
-
id: "
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
tier: "l"
|
|
1380
|
-
},
|
|
1381
|
-
name: "YOLO26 Large",
|
|
1382
|
-
description: "YOLO26 Large — maximum accuracy for demanding deployments",
|
|
1130
|
+
id: "yolo26s-seg",
|
|
1131
|
+
name: "YOLO26 Small Seg",
|
|
1132
|
+
description: "YOLO26 Small Segmentation — balanced instance segmentation",
|
|
1383
1133
|
inputSize: {
|
|
1384
1134
|
width: 640,
|
|
1385
1135
|
height: 640
|
|
@@ -1388,24 +1138,23 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1388
1138
|
preprocessMode: "letterbox",
|
|
1389
1139
|
formats: {
|
|
1390
1140
|
onnx: {
|
|
1391
|
-
url: hf("
|
|
1392
|
-
sizeMB:
|
|
1141
|
+
url: hf("segmentation/yolo26-seg/onnx/camstack-yolo26s-seg.onnx"),
|
|
1142
|
+
sizeMB: 40
|
|
1393
1143
|
},
|
|
1394
1144
|
coreml: {
|
|
1395
|
-
url: hf("
|
|
1396
|
-
sizeMB:
|
|
1145
|
+
url: hf("segmentation/yolo26-seg/coreml/camstack-yolo26s-seg.mlpackage"),
|
|
1146
|
+
sizeMB: 20,
|
|
1397
1147
|
isDirectory: true,
|
|
1398
1148
|
files: [...MLPACKAGE_FILES],
|
|
1399
1149
|
runtimes: ["python"]
|
|
1400
1150
|
},
|
|
1401
|
-
openvino: ovFormat(hf("
|
|
1151
|
+
openvino: ovFormat(hf("segmentation/yolo26-seg/openvino/camstack-yolo26s-seg.xml"), 40)
|
|
1402
1152
|
}
|
|
1403
1153
|
},
|
|
1404
1154
|
{
|
|
1405
|
-
id: "
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
description: "YOLO26 XLarge — highest accuracy, attention-based architecture",
|
|
1155
|
+
id: "yolo26m-seg",
|
|
1156
|
+
name: "YOLO26 Medium Seg",
|
|
1157
|
+
description: "YOLO26 Medium Segmentation — high-accuracy instance segmentation",
|
|
1409
1158
|
inputSize: {
|
|
1410
1159
|
width: 640,
|
|
1411
1160
|
height: 640
|
|
@@ -1414,1054 +1163,1492 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1414
1163
|
preprocessMode: "letterbox",
|
|
1415
1164
|
formats: {
|
|
1416
1165
|
onnx: {
|
|
1417
|
-
url: hf("
|
|
1418
|
-
sizeMB:
|
|
1419
|
-
},
|
|
1420
|
-
coreml: {
|
|
1421
|
-
url: hf("objectDetection/yolo26/coreml/camstack-yolo26x.mlpackage"),
|
|
1422
|
-
sizeMB: 107,
|
|
1423
|
-
isDirectory: true,
|
|
1424
|
-
files: [...MLPACKAGE_FILES],
|
|
1425
|
-
runtimes: ["python"]
|
|
1426
|
-
},
|
|
1427
|
-
openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26x.xml"), 213)
|
|
1428
|
-
}
|
|
1429
|
-
},
|
|
1430
|
-
{
|
|
1431
|
-
id: "scrypted-yolov9t-relu",
|
|
1432
|
-
legacy: true,
|
|
1433
|
-
name: "YOLOv9 Tiny ReLU",
|
|
1434
|
-
description: "YOLOv9t ReLU — optimized for int8 quantization",
|
|
1435
|
-
inputSize: {
|
|
1436
|
-
width: 320,
|
|
1437
|
-
height: 320
|
|
1438
|
-
},
|
|
1439
|
-
labels: [],
|
|
1440
|
-
preprocessMode: "letterbox",
|
|
1441
|
-
formats: {
|
|
1442
|
-
onnx: {
|
|
1443
|
-
url: hfScrypted("onnx/scrypted_yolov9t_relu/scrypted_yolov9t_relu.onnx"),
|
|
1444
|
-
sizeMB: 8.1
|
|
1166
|
+
url: hf("segmentation/yolo26-seg/onnx/camstack-yolo26m-seg.onnx"),
|
|
1167
|
+
sizeMB: 90
|
|
1445
1168
|
},
|
|
1446
1169
|
coreml: {
|
|
1447
|
-
url:
|
|
1448
|
-
sizeMB:
|
|
1170
|
+
url: hf("segmentation/yolo26-seg/coreml/camstack-yolo26m-seg.mlpackage"),
|
|
1171
|
+
sizeMB: 45,
|
|
1449
1172
|
isDirectory: true,
|
|
1450
1173
|
files: [...MLPACKAGE_FILES],
|
|
1451
1174
|
runtimes: ["python"]
|
|
1452
1175
|
},
|
|
1453
|
-
openvino: ovFormat(hf("
|
|
1176
|
+
openvino: ovFormat(hf("segmentation/yolo26-seg/openvino/camstack-yolo26m-seg.xml"), 90)
|
|
1454
1177
|
}
|
|
1455
|
-
}
|
|
1178
|
+
}
|
|
1179
|
+
];
|
|
1180
|
+
var CLIP_EMBEDDING_MODELS = [
|
|
1456
1181
|
{
|
|
1457
|
-
id: "
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
description: "YOLOv9s ReLU — improved efficiency, int8 ready",
|
|
1182
|
+
id: "mobileclip-s0",
|
|
1183
|
+
name: "MobileCLIP S0",
|
|
1184
|
+
description: "MobileCLIP S0 — Apple ultra-fast CLIP vision encoder, 512-dim, 256×256 (40 MB ONNX)",
|
|
1461
1185
|
inputSize: {
|
|
1462
|
-
width:
|
|
1463
|
-
height:
|
|
1186
|
+
width: 256,
|
|
1187
|
+
height: 256
|
|
1464
1188
|
},
|
|
1465
|
-
labels: [
|
|
1466
|
-
|
|
1189
|
+
labels: [{
|
|
1190
|
+
id: "embedding",
|
|
1191
|
+
name: "CLIP Embedding"
|
|
1192
|
+
}],
|
|
1193
|
+
preprocessMode: "resize",
|
|
1194
|
+
inputNormalization: "none",
|
|
1467
1195
|
formats: {
|
|
1468
1196
|
onnx: {
|
|
1469
|
-
url:
|
|
1470
|
-
sizeMB:
|
|
1197
|
+
url: hf("clip/mobileclip-s0/onnx/camstack-mobileclip-s0-vision.onnx"),
|
|
1198
|
+
sizeMB: 40
|
|
1471
1199
|
},
|
|
1200
|
+
openvino: ovFormat(hf("clip/mobileclip-s0/openvino/camstack-mobileclip-s0-vision.xml"), 25),
|
|
1472
1201
|
coreml: {
|
|
1473
|
-
url:
|
|
1474
|
-
sizeMB:
|
|
1202
|
+
url: hf("clip/mobileclip-s0/coreml/camstack-mobileclip-s0-vision.mlpackage"),
|
|
1203
|
+
sizeMB: 30,
|
|
1475
1204
|
isDirectory: true,
|
|
1476
1205
|
files: [...MLPACKAGE_FILES],
|
|
1477
1206
|
runtimes: ["python"]
|
|
1478
|
-
}
|
|
1479
|
-
openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9s_relu.xml"), 16)
|
|
1207
|
+
}
|
|
1480
1208
|
}
|
|
1481
1209
|
},
|
|
1482
1210
|
{
|
|
1483
|
-
id: "
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
description: "YOLOv9c ReLU — high-accuracy, int8 ready",
|
|
1211
|
+
id: "mobileclip-s1",
|
|
1212
|
+
name: "MobileCLIP S1",
|
|
1213
|
+
description: "MobileCLIP S1 — Apple balanced CLIP vision encoder, 512-dim, 256×256 (90 MB ONNX)",
|
|
1487
1214
|
inputSize: {
|
|
1488
|
-
width:
|
|
1489
|
-
height:
|
|
1215
|
+
width: 256,
|
|
1216
|
+
height: 256
|
|
1490
1217
|
},
|
|
1491
|
-
labels: [
|
|
1492
|
-
|
|
1218
|
+
labels: [{
|
|
1219
|
+
id: "embedding",
|
|
1220
|
+
name: "CLIP Embedding"
|
|
1221
|
+
}],
|
|
1222
|
+
preprocessMode: "resize",
|
|
1223
|
+
inputNormalization: "none",
|
|
1493
1224
|
formats: {
|
|
1494
1225
|
onnx: {
|
|
1495
|
-
url:
|
|
1496
|
-
sizeMB:
|
|
1226
|
+
url: hf("clip/mobileclip-s1/onnx/camstack-mobileclip-s1-vision.onnx"),
|
|
1227
|
+
sizeMB: 90
|
|
1497
1228
|
},
|
|
1229
|
+
openvino: ovFormat(hf("clip/mobileclip-s1/openvino/camstack-mobileclip-s1-vision.xml"), 55),
|
|
1498
1230
|
coreml: {
|
|
1499
|
-
url:
|
|
1500
|
-
sizeMB:
|
|
1231
|
+
url: hf("clip/mobileclip-s1/coreml/camstack-mobileclip-s1-vision.mlpackage"),
|
|
1232
|
+
sizeMB: 65,
|
|
1501
1233
|
isDirectory: true,
|
|
1502
1234
|
files: [...MLPACKAGE_FILES],
|
|
1503
1235
|
runtimes: ["python"]
|
|
1504
|
-
}
|
|
1505
|
-
openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9c_relu.xml"), 49)
|
|
1236
|
+
}
|
|
1506
1237
|
}
|
|
1507
1238
|
},
|
|
1508
1239
|
{
|
|
1509
|
-
id: "
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
description: "YOLOv9m ReLU — medium, int8 ready",
|
|
1240
|
+
id: "mobileclip-s2",
|
|
1241
|
+
name: "MobileCLIP S2",
|
|
1242
|
+
description: "MobileCLIP S2 — Apple high-accuracy CLIP vision encoder, 512-dim, 256×256 (150 MB ONNX)",
|
|
1513
1243
|
inputSize: {
|
|
1514
|
-
width:
|
|
1515
|
-
height:
|
|
1244
|
+
width: 256,
|
|
1245
|
+
height: 256
|
|
1516
1246
|
},
|
|
1517
|
-
labels: [
|
|
1518
|
-
|
|
1247
|
+
labels: [{
|
|
1248
|
+
id: "embedding",
|
|
1249
|
+
name: "CLIP Embedding"
|
|
1250
|
+
}],
|
|
1251
|
+
preprocessMode: "resize",
|
|
1252
|
+
inputNormalization: "none",
|
|
1519
1253
|
formats: {
|
|
1520
1254
|
onnx: {
|
|
1521
|
-
url:
|
|
1522
|
-
sizeMB:
|
|
1255
|
+
url: hf("clip/mobileclip-s2/onnx/camstack-mobileclip-s2-vision.onnx"),
|
|
1256
|
+
sizeMB: 150
|
|
1523
1257
|
},
|
|
1258
|
+
openvino: ovFormat(hf("clip/mobileclip-s2/openvino/camstack-mobileclip-s2-vision.xml"), 90),
|
|
1524
1259
|
coreml: {
|
|
1525
|
-
url:
|
|
1526
|
-
sizeMB:
|
|
1260
|
+
url: hf("clip/mobileclip-s2/coreml/camstack-mobileclip-s2-vision.mlpackage"),
|
|
1261
|
+
sizeMB: 110,
|
|
1527
1262
|
isDirectory: true,
|
|
1528
1263
|
files: [...MLPACKAGE_FILES],
|
|
1529
1264
|
runtimes: ["python"]
|
|
1530
|
-
}
|
|
1531
|
-
openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9m_relu.xml"), 38)
|
|
1265
|
+
}
|
|
1532
1266
|
}
|
|
1533
|
-
},
|
|
1534
|
-
ovPrecisionVariant("yolov9t", "objectDetection/yolov9/openvino", "YOLOv9 Tiny", "fp16", 5, true),
|
|
1535
|
-
ovPrecisionVariant("yolov9t", "objectDetection/yolov9/openvino", "YOLOv9 Tiny", "int8", 3, true),
|
|
1536
|
-
ovPrecisionVariant("yolov9s", "objectDetection/yolov9/openvino", "YOLOv9 Small", "fp16", 15, true),
|
|
1537
|
-
ovPrecisionVariant("yolov9s", "objectDetection/yolov9/openvino", "YOLOv9 Small", "int8", 8, true),
|
|
1538
|
-
ovPrecisionVariant("yolo26n", "objectDetection/yolo26/openvino", "YOLO26 Nano", "fp16", 5, true),
|
|
1539
|
-
ovPrecisionVariant("yolo26n", "objectDetection/yolo26/openvino", "YOLO26 Nano", "int8", 3),
|
|
1540
|
-
ovPrecisionVariant("yolo26s", "objectDetection/yolo26/openvino", "YOLO26 Small", "fp16", 19, true),
|
|
1541
|
-
ovPrecisionVariant("yolo26s", "objectDetection/yolo26/openvino", "YOLO26 Small", "int8", 10),
|
|
1542
|
-
ovPrecisionVariant("yolo26m", "objectDetection/yolo26/openvino", "YOLO26 Medium", "fp16", 41, true),
|
|
1543
|
-
ovPrecisionVariant("yolo26m", "objectDetection/yolo26/openvino", "YOLO26 Medium", "int8", 21),
|
|
1544
|
-
ovPrecisionVariant("yolo26l", "objectDetection/yolo26/openvino", "YOLO26 Large", "fp16", 50, true),
|
|
1545
|
-
ovPrecisionVariant("yolo26l", "objectDetection/yolo26/openvino", "YOLO26 Large", "int8", 25),
|
|
1546
|
-
ovPrecisionVariant("yolo26x", "objectDetection/yolo26/openvino", "YOLO26 XLarge", "fp16", 112, true),
|
|
1547
|
-
ovPrecisionVariant("yolo26x", "objectDetection/yolo26/openvino", "YOLO26 XLarge", "int8", 56, true)
|
|
1548
|
-
];
|
|
1549
|
-
var FACE_DETECTION_MODELS = [{
|
|
1550
|
-
id: "scrfd-2.5g",
|
|
1551
|
-
name: "SCRFD 2.5G",
|
|
1552
|
-
description: "SCRFD 2.5G — balanced face detection model",
|
|
1553
|
-
inputSize: {
|
|
1554
|
-
width: 640,
|
|
1555
|
-
height: 640
|
|
1556
|
-
},
|
|
1557
|
-
labels: [{
|
|
1558
|
-
id: "face",
|
|
1559
|
-
name: "Face"
|
|
1560
|
-
}],
|
|
1561
|
-
preprocessMode: "letterbox",
|
|
1562
|
-
formats: {
|
|
1563
|
-
onnx: {
|
|
1564
|
-
url: hf("faceDetection/scrfd/onnx/camstack-scrfd-2.5g.onnx"),
|
|
1565
|
-
sizeMB: 3.1
|
|
1566
|
-
},
|
|
1567
|
-
coreml: {
|
|
1568
|
-
url: hf("faceDetection/scrfd/coreml/camstack-scrfd-2.5g.mlpackage"),
|
|
1569
|
-
sizeMB: 1.7,
|
|
1570
|
-
isDirectory: true,
|
|
1571
|
-
files: [...MLPACKAGE_FILES],
|
|
1572
|
-
runtimes: ["python"]
|
|
1573
|
-
},
|
|
1574
|
-
openvino: ovFormat(hf("faceDetection/scrfd/openvino/camstack-scrfd-2.5g.xml"), 1.8)
|
|
1575
|
-
}
|
|
1576
|
-
}, {
|
|
1577
|
-
id: "scrypted-yolov9t-face",
|
|
1578
|
-
name: "YOLOv9t Face ReLU",
|
|
1579
|
-
description: "YOLOv9t face detection — YOLO-based, fast, 320x320 ReLU",
|
|
1580
|
-
inputSize: {
|
|
1581
|
-
width: 320,
|
|
1582
|
-
height: 320
|
|
1583
|
-
},
|
|
1584
|
-
labels: [{
|
|
1585
|
-
id: "face",
|
|
1586
|
-
name: "Face"
|
|
1587
|
-
}],
|
|
1588
|
-
preprocessMode: "letterbox",
|
|
1589
|
-
formats: {
|
|
1590
|
-
onnx: {
|
|
1591
|
-
url: hfScrypted("onnx/scrypted_yolov9t_relu_face/scrypted_yolov9t_relu_face.onnx"),
|
|
1592
|
-
sizeMB: 8.1
|
|
1593
|
-
},
|
|
1594
|
-
coreml: {
|
|
1595
|
-
url: hfScrypted("coreml/scrypted_yolov9t_relu_face/scrypted_yolov9t_relu_face.mlpackage"),
|
|
1596
|
-
sizeMB: 4,
|
|
1597
|
-
isDirectory: true,
|
|
1598
|
-
files: [...MLPACKAGE_FILES],
|
|
1599
|
-
runtimes: ["python"]
|
|
1600
|
-
},
|
|
1601
|
-
openvino: ovFormat(hf("faceDetection/scrypted-yolov9-face/openvino/scrypted_yolov9t_relu_face.xml"), 6)
|
|
1602
1267
|
}
|
|
1603
|
-
|
|
1604
|
-
var
|
|
1605
|
-
id: "
|
|
1606
|
-
name: "
|
|
1607
|
-
description: "
|
|
1268
|
+
];
|
|
1269
|
+
var AUDIO_CLASSIFIER_MODELS = [{
|
|
1270
|
+
id: "yamnet-onnx",
|
|
1271
|
+
name: "YAMNet",
|
|
1272
|
+
description: "Google YAMNet — 521-class audio event classifier (3.2 MB ONNX, runs on any platform)",
|
|
1608
1273
|
inputSize: {
|
|
1609
|
-
width:
|
|
1610
|
-
height:
|
|
1274
|
+
width: 1,
|
|
1275
|
+
height: 16e3
|
|
1611
1276
|
},
|
|
1612
|
-
|
|
1613
|
-
labels: [{
|
|
1614
|
-
id: "embedding",
|
|
1615
|
-
name: "Face Embedding"
|
|
1616
|
-
}],
|
|
1277
|
+
labels: [],
|
|
1617
1278
|
preprocessMode: "resize",
|
|
1618
|
-
faceAlignment: true,
|
|
1619
1279
|
formats: {
|
|
1620
1280
|
onnx: {
|
|
1621
|
-
url: hf("
|
|
1622
|
-
sizeMB:
|
|
1281
|
+
url: hf("audioClassification/yamnet/onnx/camstack-yamnet.onnx"),
|
|
1282
|
+
sizeMB: 3.2
|
|
1623
1283
|
},
|
|
1284
|
+
openvino: ovFormat(hf("audioClassification/yamnet/openvino/camstack-yamnet.xml"), 3.2),
|
|
1624
1285
|
coreml: {
|
|
1625
|
-
url: hf("
|
|
1626
|
-
sizeMB:
|
|
1627
|
-
isDirectory: true,
|
|
1628
|
-
files: [...MLPACKAGE_FILES],
|
|
1286
|
+
url: hf("audioClassification/yamnet/onnx/camstack-yamnet.onnx"),
|
|
1287
|
+
sizeMB: 3.2,
|
|
1629
1288
|
runtimes: ["python"]
|
|
1630
|
-
}
|
|
1631
|
-
openvino: ovFormat(hf("faceRecognition/arcface/openvino/camstack-arcface-r100.xml"), 65)
|
|
1289
|
+
}
|
|
1632
1290
|
}
|
|
1633
1291
|
}, {
|
|
1634
|
-
id: "
|
|
1635
|
-
name: "
|
|
1636
|
-
description: "
|
|
1292
|
+
id: "apple-soundanalysis",
|
|
1293
|
+
name: "Apple SoundAnalysis",
|
|
1294
|
+
description: "macOS built-in — 303 sound categories, Neural Engine accelerated, zero download",
|
|
1637
1295
|
inputSize: {
|
|
1638
|
-
width:
|
|
1639
|
-
height:
|
|
1296
|
+
width: 1,
|
|
1297
|
+
height: 16e3
|
|
1640
1298
|
},
|
|
1641
|
-
labels: [
|
|
1642
|
-
id: "embedding",
|
|
1643
|
-
name: "Face Embedding"
|
|
1644
|
-
}],
|
|
1299
|
+
labels: [],
|
|
1645
1300
|
preprocessMode: "resize",
|
|
1646
|
-
formats: {
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1301
|
+
formats: { coreml: {
|
|
1302
|
+
url: "",
|
|
1303
|
+
sizeMB: 0
|
|
1304
|
+
} }
|
|
1305
|
+
}];
|
|
1306
|
+
//#endregion
|
|
1307
|
+
//#region src/detection-pipeline/registry/step-definitions.ts
|
|
1308
|
+
var ANIMAL_10_LABELS = [
|
|
1309
|
+
"cat",
|
|
1310
|
+
"cow",
|
|
1311
|
+
"dog",
|
|
1312
|
+
"dolphin",
|
|
1313
|
+
"eagle",
|
|
1314
|
+
"giant panda",
|
|
1315
|
+
"horse",
|
|
1316
|
+
"monkey",
|
|
1317
|
+
"sheep",
|
|
1318
|
+
"spider"
|
|
1319
|
+
];
|
|
1320
|
+
var VGG_G2_CHARSET = ["", ..."0123456789!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split("")];
|
|
1321
|
+
var PipelineStepBase = class {
|
|
1322
|
+
definition;
|
|
1323
|
+
constructor(definition) {
|
|
1324
|
+
this.definition = definition;
|
|
1325
|
+
}
|
|
1326
|
+
getConfigSchema() {
|
|
1327
|
+
return [];
|
|
1328
|
+
}
|
|
1329
|
+
};
|
|
1330
|
+
var ObjectDetectionStep = class {
|
|
1331
|
+
definition = {
|
|
1332
|
+
id: "object-detection",
|
|
1333
|
+
name: "Object Detection",
|
|
1334
|
+
slot: "detector",
|
|
1335
|
+
postprocessor: "yolo",
|
|
1336
|
+
extractMode: "full-frame",
|
|
1337
|
+
inputClasses: null,
|
|
1338
|
+
outputClasses: [
|
|
1339
|
+
"person",
|
|
1340
|
+
"vehicle",
|
|
1341
|
+
"animal"
|
|
1342
|
+
],
|
|
1343
|
+
models: [...OBJECT_DETECTION_MODELS],
|
|
1344
|
+
defaultModelId: "yolo26n",
|
|
1345
|
+
defaultConfidence: .5,
|
|
1346
|
+
labels: require_dist.COCO_80_LABELS.map((l) => l.id),
|
|
1347
|
+
classMap: require_dist.COCO_TO_MACRO
|
|
1348
|
+
};
|
|
1349
|
+
getConfigSchema() {
|
|
1350
|
+
return [
|
|
1351
|
+
{
|
|
1352
|
+
type: "multiselect",
|
|
1353
|
+
key: "enabledMacroClasses",
|
|
1354
|
+
label: "Enabled Classes",
|
|
1355
|
+
description: "Which macro classes to detect (empty = all three).",
|
|
1356
|
+
options: [
|
|
1357
|
+
{
|
|
1358
|
+
value: "person",
|
|
1359
|
+
label: "Person"
|
|
1360
|
+
},
|
|
1361
|
+
{
|
|
1362
|
+
value: "vehicle",
|
|
1363
|
+
label: "Vehicle"
|
|
1364
|
+
},
|
|
1365
|
+
{
|
|
1366
|
+
value: "animal",
|
|
1367
|
+
label: "Animal"
|
|
1368
|
+
}
|
|
1369
|
+
],
|
|
1370
|
+
default: [
|
|
1371
|
+
"person",
|
|
1372
|
+
"vehicle",
|
|
1373
|
+
"animal"
|
|
1374
|
+
]
|
|
1375
|
+
},
|
|
1376
|
+
{
|
|
1377
|
+
type: "slider",
|
|
1378
|
+
key: "minConfidencePerson",
|
|
1379
|
+
label: "Min Confidence — Person",
|
|
1380
|
+
description: "Drop person predictions below this score.",
|
|
1381
|
+
min: 0,
|
|
1382
|
+
max: 1,
|
|
1383
|
+
step: .05,
|
|
1384
|
+
default: .5,
|
|
1385
|
+
showValue: true
|
|
1386
|
+
},
|
|
1387
|
+
{
|
|
1388
|
+
type: "slider",
|
|
1389
|
+
key: "minConfidenceVehicle",
|
|
1390
|
+
label: "Min Confidence — Vehicle",
|
|
1391
|
+
description: "Drop vehicle predictions below this score.",
|
|
1392
|
+
min: 0,
|
|
1393
|
+
max: 1,
|
|
1394
|
+
step: .05,
|
|
1395
|
+
default: .5,
|
|
1396
|
+
showValue: true
|
|
1397
|
+
},
|
|
1398
|
+
{
|
|
1399
|
+
type: "slider",
|
|
1400
|
+
key: "minConfidenceAnimal",
|
|
1401
|
+
label: "Min Confidence — Animal",
|
|
1402
|
+
description: "Drop animal predictions below this score.",
|
|
1403
|
+
min: 0,
|
|
1404
|
+
max: 1,
|
|
1405
|
+
step: .05,
|
|
1406
|
+
default: .5,
|
|
1407
|
+
showValue: true
|
|
1408
|
+
}
|
|
1409
|
+
];
|
|
1659
1410
|
}
|
|
1660
|
-
}
|
|
1661
|
-
var
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1411
|
+
};
|
|
1412
|
+
var AudioClassifierStep = class {
|
|
1413
|
+
definition = {
|
|
1414
|
+
id: "audio-classifier",
|
|
1415
|
+
name: "Audio Classifier",
|
|
1416
|
+
slot: "audio-classifier",
|
|
1417
|
+
postprocessor: "yamnet",
|
|
1418
|
+
extractMode: "full-frame",
|
|
1419
|
+
inputClasses: null,
|
|
1420
|
+
outputClasses: ["audio"],
|
|
1421
|
+
models: [...AUDIO_CLASSIFIER_MODELS],
|
|
1422
|
+
defaultModelId: "yamnet-onnx",
|
|
1423
|
+
defaultModelIdByFormat: { coreml: "apple-soundanalysis" },
|
|
1424
|
+
enabledByDefault: true,
|
|
1425
|
+
defaultConfidence: .3
|
|
1426
|
+
};
|
|
1427
|
+
getConfigSchema() {
|
|
1428
|
+
return [{
|
|
1429
|
+
type: "multiselect",
|
|
1430
|
+
key: "enabledAudioClasses",
|
|
1431
|
+
label: "Audio Classes",
|
|
1432
|
+
description: "Which audio macro classes to surface (empty = all).",
|
|
1433
|
+
options: require_dist.AUDIO_MACRO_LABELS.map((l) => ({
|
|
1434
|
+
value: l.id,
|
|
1435
|
+
label: `${l.icon ?? ""} ${l.name}`.trim()
|
|
1436
|
+
})),
|
|
1437
|
+
default: [
|
|
1438
|
+
"scream",
|
|
1439
|
+
"crying",
|
|
1440
|
+
"dog",
|
|
1441
|
+
"alarm",
|
|
1442
|
+
"doorbell",
|
|
1443
|
+
"glass_breaking",
|
|
1444
|
+
"gunshot",
|
|
1445
|
+
"siren",
|
|
1446
|
+
"footsteps"
|
|
1447
|
+
]
|
|
1448
|
+
}, {
|
|
1449
|
+
type: "slider",
|
|
1450
|
+
key: "minConfidence",
|
|
1451
|
+
label: "Min Confidence",
|
|
1452
|
+
description: "Drop predictions whose score is below this threshold.",
|
|
1453
|
+
default: .3,
|
|
1454
|
+
min: 0,
|
|
1455
|
+
max: 1,
|
|
1456
|
+
step: .05,
|
|
1457
|
+
showValue: true
|
|
1458
|
+
}];
|
|
1687
1459
|
}
|
|
1688
|
-
}
|
|
1689
|
-
var
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1460
|
+
};
|
|
1461
|
+
var AnimalClassifierStep = class {
|
|
1462
|
+
definition = {
|
|
1463
|
+
id: "animal-classifier",
|
|
1464
|
+
name: "Animal Classifier",
|
|
1465
|
+
slot: "classifier",
|
|
1466
|
+
postprocessor: "softmax",
|
|
1467
|
+
extractMode: "crop-roi",
|
|
1468
|
+
inputClasses: ["animal"],
|
|
1469
|
+
outputClasses: ["animal-type"],
|
|
1470
|
+
models: [...ANIMAL_CLASSIFIER_MODELS],
|
|
1471
|
+
defaultModelId: "animals-10",
|
|
1472
|
+
enabledByDefault: false,
|
|
1473
|
+
defaultConfidence: .2,
|
|
1474
|
+
labels: [...ANIMAL_10_LABELS]
|
|
1475
|
+
};
|
|
1476
|
+
getConfigSchema() {
|
|
1477
|
+
return [{
|
|
1478
|
+
type: "multiselect",
|
|
1479
|
+
key: "enabledClasses",
|
|
1480
|
+
label: "Species",
|
|
1481
|
+
description: "Which animal species to keep (empty = all). The classifier still scores every label but only the selected classes are returned on the detection.",
|
|
1482
|
+
options: ANIMAL_10_LABELS.map((l) => ({
|
|
1483
|
+
value: l,
|
|
1484
|
+
label: l
|
|
1485
|
+
})),
|
|
1486
|
+
default: []
|
|
1487
|
+
}, {
|
|
1488
|
+
type: "slider",
|
|
1489
|
+
key: "minConfidence",
|
|
1490
|
+
label: "Min Confidence",
|
|
1491
|
+
description: "Drop classifications whose score is below this threshold.",
|
|
1492
|
+
default: this.definition.defaultConfidence,
|
|
1493
|
+
min: 0,
|
|
1494
|
+
max: 1,
|
|
1495
|
+
step: .05,
|
|
1496
|
+
showValue: true
|
|
1497
|
+
}];
|
|
1715
1498
|
}
|
|
1716
|
-
}
|
|
1717
|
-
var
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1499
|
+
};
|
|
1500
|
+
var ClassifierWithMinConfidence = class extends PipelineStepBase {
|
|
1501
|
+
getConfigSchema() {
|
|
1502
|
+
return [{
|
|
1503
|
+
type: "slider",
|
|
1504
|
+
key: "minConfidence",
|
|
1505
|
+
label: "Min Confidence",
|
|
1506
|
+
description: "Drop classifications whose score is below this threshold.",
|
|
1507
|
+
default: this.definition.defaultConfidence,
|
|
1508
|
+
min: 0,
|
|
1509
|
+
max: 1,
|
|
1510
|
+
step: .05,
|
|
1511
|
+
showValue: true
|
|
1512
|
+
}];
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1515
|
+
var STEP_FACE_DETECTION = new PipelineStepBase({
|
|
1516
|
+
id: "face-detection",
|
|
1517
|
+
name: "Face Detection",
|
|
1518
|
+
slot: "cropper",
|
|
1519
|
+
postprocessor: "scrfd",
|
|
1520
|
+
extractMode: "crop-roi",
|
|
1521
|
+
inputClasses: ["person"],
|
|
1522
|
+
outputClasses: ["face"],
|
|
1523
|
+
models: [...FACE_DETECTION_MODELS],
|
|
1524
|
+
defaultModelId: "scrfd-2.5g",
|
|
1525
|
+
defaultConfidence: .5,
|
|
1526
|
+
defaultMinParentScore: .7
|
|
1527
|
+
});
|
|
1528
|
+
var STEP_FACE_EMBEDDING = new PipelineStepBase({
|
|
1529
|
+
id: "face-embedding",
|
|
1530
|
+
name: "Face Embedding",
|
|
1531
|
+
slot: "classifier",
|
|
1532
|
+
postprocessor: "arcface",
|
|
1533
|
+
extractMode: "crop-roi",
|
|
1534
|
+
inputClasses: ["face"],
|
|
1535
|
+
outputClasses: ["identity"],
|
|
1536
|
+
models: [...FACE_EMBEDDING_MODELS],
|
|
1537
|
+
defaultModelId: "arcface-r100",
|
|
1538
|
+
defaultConfidence: 0
|
|
1539
|
+
});
|
|
1540
|
+
var STEP_CLIP_EMBEDDING = new PipelineStepBase({
|
|
1541
|
+
id: "clip-embedding",
|
|
1542
|
+
name: "CLIP Embedding",
|
|
1543
|
+
slot: "classifier",
|
|
1544
|
+
postprocessor: "clip",
|
|
1545
|
+
extractMode: "crop-roi",
|
|
1546
|
+
inputClasses: null,
|
|
1547
|
+
outputClasses: ["embedding"],
|
|
1548
|
+
models: [...CLIP_EMBEDDING_MODELS],
|
|
1549
|
+
defaultModelId: "mobileclip-s1",
|
|
1550
|
+
enabledByDefault: false,
|
|
1551
|
+
defaultConfidence: 0,
|
|
1552
|
+
defaultMinParentScore: .4,
|
|
1553
|
+
group: "CLIP"
|
|
1554
|
+
});
|
|
1555
|
+
var STEP_PLATE_DETECTION = new PipelineStepBase({
|
|
1556
|
+
id: "plate-detection",
|
|
1557
|
+
name: "Plate Detection",
|
|
1558
|
+
slot: "cropper",
|
|
1559
|
+
postprocessor: "yolo",
|
|
1560
|
+
extractMode: "crop-roi",
|
|
1561
|
+
inputClasses: ["vehicle"],
|
|
1562
|
+
outputClasses: ["plate"],
|
|
1563
|
+
models: [...PLATE_DETECTION_MODELS],
|
|
1564
|
+
defaultModelId: "yolov8n-plate",
|
|
1565
|
+
defaultConfidence: .5,
|
|
1566
|
+
defaultMinParentScore: .7,
|
|
1567
|
+
labels: ["plate"]
|
|
1568
|
+
});
|
|
1569
|
+
var STEP_PLATE_OCR = new PipelineStepBase({
|
|
1570
|
+
id: "plate-ocr",
|
|
1571
|
+
name: "Plate OCR",
|
|
1572
|
+
slot: "classifier",
|
|
1573
|
+
postprocessor: "ctc",
|
|
1574
|
+
extractMode: "crop-roi",
|
|
1575
|
+
inputClasses: ["plate"],
|
|
1576
|
+
outputClasses: ["plate-text"],
|
|
1577
|
+
models: [...PLATE_OCR_MODELS],
|
|
1578
|
+
defaultModelId: "vgg-english-g2",
|
|
1579
|
+
defaultConfidence: 0,
|
|
1580
|
+
charset: [...VGG_G2_CHARSET]
|
|
1581
|
+
});
|
|
1582
|
+
var STEP_BIRD_CLASSIFIER = new ClassifierWithMinConfidence({
|
|
1583
|
+
id: "bird-classifier",
|
|
1584
|
+
name: "Bird Classifier",
|
|
1585
|
+
slot: "classifier",
|
|
1586
|
+
postprocessor: "softmax",
|
|
1587
|
+
extractMode: "crop-roi",
|
|
1588
|
+
inputClasses: ["animal"],
|
|
1589
|
+
outputClasses: ["species"],
|
|
1590
|
+
models: [...BIRD_CLASSIFIER_MODELS],
|
|
1591
|
+
defaultModelId: "bird-nabirds-404",
|
|
1592
|
+
enabledByDefault: false,
|
|
1593
|
+
defaultConfidence: .2
|
|
1594
|
+
});
|
|
1595
|
+
var STEP_VEHICLE_CLASSIFIER = new ClassifierWithMinConfidence({
|
|
1596
|
+
id: "vehicle-classifier",
|
|
1597
|
+
name: "Vehicle Classifier",
|
|
1598
|
+
slot: "classifier",
|
|
1599
|
+
postprocessor: "softmax",
|
|
1600
|
+
extractMode: "crop-roi",
|
|
1601
|
+
inputClasses: ["vehicle"],
|
|
1602
|
+
outputClasses: ["vehicle-type"],
|
|
1603
|
+
models: [...VEHICLE_CLASSIFIER_MODELS],
|
|
1604
|
+
defaultModelId: "vehicle-type-efficientnet",
|
|
1605
|
+
enabledByDefault: false,
|
|
1606
|
+
defaultConfidence: .3
|
|
1607
|
+
});
|
|
1608
|
+
var STEP_SEGMENTATION = new PipelineStepBase({
|
|
1609
|
+
id: "segmentation",
|
|
1610
|
+
name: "Segmentation",
|
|
1611
|
+
slot: "refiner",
|
|
1612
|
+
postprocessor: "saliency",
|
|
1613
|
+
extractMode: "crop-roi",
|
|
1614
|
+
inputClasses: null,
|
|
1615
|
+
outputClasses: ["mask"],
|
|
1616
|
+
models: [...SEGMENTATION_REFINER_MODELS],
|
|
1617
|
+
defaultModelId: "u2netp",
|
|
1618
|
+
enabledByDefault: false,
|
|
1619
|
+
defaultConfidence: 0,
|
|
1620
|
+
group: "Segmentation"
|
|
1621
|
+
});
|
|
1622
|
+
new PipelineStepBase({
|
|
1623
|
+
id: "instance-segmentation",
|
|
1624
|
+
name: "Instance Segmentation",
|
|
1625
|
+
slot: "refiner",
|
|
1626
|
+
postprocessor: "yolo-seg",
|
|
1627
|
+
extractMode: "full-frame",
|
|
1628
|
+
inputClasses: null,
|
|
1629
|
+
outputClasses: ["mask"],
|
|
1630
|
+
models: [...INSTANCE_SEGMENTATION_MODELS],
|
|
1631
|
+
defaultModelId: "yolo26n-seg",
|
|
1632
|
+
enabledByDefault: false,
|
|
1633
|
+
defaultConfidence: .25,
|
|
1634
|
+
labels: require_dist.COCO_80_LABELS.map((l) => l.id),
|
|
1635
|
+
classMap: require_dist.COCO_TO_MACRO,
|
|
1636
|
+
group: "Segmentation"
|
|
1637
|
+
});
|
|
1638
|
+
var STEP_OBJECT_DETECTION_INSTANCE = new ObjectDetectionStep();
|
|
1639
|
+
var STEP_AUDIO_CLASSIFIER_INSTANCE = new AudioClassifierStep();
|
|
1640
|
+
var ALL_PIPELINE_STEPS = [
|
|
1641
|
+
STEP_OBJECT_DETECTION_INSTANCE,
|
|
1642
|
+
STEP_FACE_DETECTION,
|
|
1643
|
+
STEP_FACE_EMBEDDING,
|
|
1644
|
+
STEP_CLIP_EMBEDDING,
|
|
1645
|
+
STEP_PLATE_DETECTION,
|
|
1646
|
+
STEP_PLATE_OCR,
|
|
1647
|
+
new AnimalClassifierStep(),
|
|
1648
|
+
STEP_BIRD_CLASSIFIER,
|
|
1649
|
+
STEP_VEHICLE_CLASSIFIER,
|
|
1650
|
+
STEP_SEGMENTATION,
|
|
1651
|
+
STEP_AUDIO_CLASSIFIER_INSTANCE
|
|
1652
|
+
];
|
|
1653
|
+
/** Compat: flat array of StepDefinition for existing consumers */
|
|
1654
|
+
var ALL_STEPS = ALL_PIPELINE_STEPS.map((s) => s.definition);
|
|
1655
|
+
var STEP_MAP = new Map(ALL_PIPELINE_STEPS.map((s) => [s.definition.id, s]));
|
|
1656
|
+
/**
|
|
1657
|
+
* Look up a pipeline step by ID (class instance with getConfigSchema).
|
|
1658
|
+
* @throws if the step ID is not registered.
|
|
1659
|
+
*/
|
|
1660
|
+
function getStep(stepId) {
|
|
1661
|
+
const step = STEP_MAP.get(stepId);
|
|
1662
|
+
if (!step) throw new Error(`Unknown pipeline step: "${stepId}". Available: ${ALL_PIPELINE_STEPS.map((s) => s.definition.id).join(", ")}`);
|
|
1663
|
+
return step;
|
|
1664
|
+
}
|
|
1665
|
+
/**
|
|
1666
|
+
* Look up a step definition by ID (compat shortcut).
|
|
1667
|
+
* @throws if the step ID is not registered.
|
|
1668
|
+
*/
|
|
1669
|
+
function getStepDefinition(stepId) {
|
|
1670
|
+
return getStep(stepId).definition;
|
|
1671
|
+
}
|
|
1672
|
+
/**
|
|
1673
|
+
* Get the default model ID for a step given the current model format.
|
|
1674
|
+
*
|
|
1675
|
+
* Resolution order:
|
|
1676
|
+
* 1. `def.defaultModelIdByFormat?.[format]` — the step's declared
|
|
1677
|
+
* per-format preference — if that model exists in `def.models` AND
|
|
1678
|
+
* has a build for `format`.
|
|
1679
|
+
* 2. `def.defaultModelId` — the step's plain declared default — if it
|
|
1680
|
+
* exists in `def.models` AND has a build for `format`.
|
|
1681
|
+
* 3. The smallest-by-size model among those with a `format` build
|
|
1682
|
+
* (legacy fallback, preserved for steps/formats with no declared
|
|
1683
|
+
* preference reachable).
|
|
1684
|
+
* 4. `def.defaultModelId` unchanged, when ZERO models have a `format`
|
|
1685
|
+
* build — an unloadable case flagged elsewhere, not resolved here.
|
|
1686
|
+
*/
|
|
1687
|
+
function getDefaultModelForFormat(stepId, format) {
|
|
1688
|
+
const def = getStepDefinition(stepId);
|
|
1689
|
+
const hasFormatBuild = (modelId) => def.models.find((m) => m.id === modelId)?.formats[format] !== void 0;
|
|
1690
|
+
const declaredForFormat = def.defaultModelIdByFormat?.[format];
|
|
1691
|
+
if (declaredForFormat !== void 0 && hasFormatBuild(declaredForFormat)) return declaredForFormat;
|
|
1692
|
+
if (hasFormatBuild(def.defaultModelId)) return def.defaultModelId;
|
|
1693
|
+
const available = def.models.filter((m) => m.formats[format] && m.legacy !== true);
|
|
1694
|
+
if (available.length === 0) return def.defaultModelId;
|
|
1695
|
+
return [...available].toSorted((a, b) => {
|
|
1696
|
+
return (a.formats[format]?.sizeMB ?? Infinity) - (b.formats[format]?.sizeMB ?? Infinity);
|
|
1697
|
+
})[0].id;
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* The single node-local authority for turning a CHOSEN model (or none) into a
|
|
1701
|
+
* concrete model that has a build for this node's engine `format`. Fuses the
|
|
1702
|
+
* two previously-duplicated decisions (orchestrator `pickDefaultModelId` +
|
|
1703
|
+
* provider `substituteIncompatibleModels`):
|
|
1704
|
+
*
|
|
1705
|
+
* - chosen === null → the addon's default model for `format`.
|
|
1706
|
+
* - chosen is a catalog model → keep it if it has a `format` build; else
|
|
1707
|
+
* the addon's default model for `format`
|
|
1708
|
+
* (a substitution — the caller surfaces it).
|
|
1709
|
+
* - chosen is NOT in the catalog (custom/unknown) → returned unchanged; the
|
|
1710
|
+
* operator owns a custom model's format compat.
|
|
1711
|
+
* - unknown stepId → chosen returned unchanged (never throws).
|
|
1712
|
+
*/
|
|
1713
|
+
function resolveModelForFormat(stepId, chosenModelId, format) {
|
|
1714
|
+
let def;
|
|
1715
|
+
try {
|
|
1716
|
+
def = getStepDefinition(stepId);
|
|
1717
|
+
} catch {
|
|
1718
|
+
return chosenModelId ?? "";
|
|
1744
1719
|
}
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1720
|
+
if (chosenModelId === null) return getDefaultModelForFormat(stepId, format);
|
|
1721
|
+
const entry = def.models.find((m) => m.id === chosenModelId);
|
|
1722
|
+
if (!entry) return chosenModelId;
|
|
1723
|
+
if (entry.formats[format]) return chosenModelId;
|
|
1724
|
+
return getDefaultModelForFormat(stepId, format);
|
|
1725
|
+
}
|
|
1726
|
+
//#endregion
|
|
1727
|
+
//#region src/detection-pipeline/default-detection-model.ts
|
|
1728
|
+
/** The object-detection step id — the only slot this resolver applies to. */
|
|
1729
|
+
var OBJECT_DETECTION_STEP_ID = "object-detection";
|
|
1730
|
+
/**
|
|
1731
|
+
* Balanced default object-detection model per accelerator class (measured on
|
|
1732
|
+
* COCO val2017). `'cpu'` maps to `null` → the caller substitutes the step's
|
|
1733
|
+
* own `defaultModelId` (`yolo26n`), the safe nano that does not collapse on
|
|
1734
|
+
* small objects. The three accelerated ids are reduced-input YOLO26 variants
|
|
1735
|
+
* shipped in the catalog (onnx + coreml + openvino builds each).
|
|
1736
|
+
*/
|
|
1737
|
+
var MODEL_BY_CLASS = {
|
|
1738
|
+
"apple-ane": "yolo26l-256",
|
|
1739
|
+
"intel-npu": "yolo26m-320",
|
|
1740
|
+
"intel-gpu": "yolo26m-256",
|
|
1741
|
+
cpu: null
|
|
1742
|
+
};
|
|
1743
|
+
/**
|
|
1744
|
+
* Classify a node's engine into an {@link AcceleratorClass} from its resolved
|
|
1745
|
+
* `backend` + `device`. Pure, total, never throws.
|
|
1746
|
+
*
|
|
1747
|
+
* OpenVINO's default device is `'auto'` (the python pool resolves NPU > GPU >
|
|
1748
|
+
* CPU at load). Because OpenVINO is only ever OFFERED when an Intel iGPU is
|
|
1749
|
+
* present, the GPU is the guaranteed accelerator while the NPU is an explicit
|
|
1750
|
+
* opt-in — so `'auto'` (and any non-cpu/non-npu device) is treated as
|
|
1751
|
+
* GPU-class conservatively. An operator who explicitly pins `device: 'npu'`
|
|
1752
|
+
* gets the NPU-tuned m-320.
|
|
1753
|
+
*
|
|
1754
|
+
* Also accepts the accelerator carried in the `backend` slot (e.g. `'ane'`,
|
|
1755
|
+
* `'mps'`, `'npu'`, `'intel-gpu'`) so a caller passing a device-style backend
|
|
1756
|
+
* still classifies correctly — real callers pass onnx/openvino/coreml.
|
|
1757
|
+
*/
|
|
1758
|
+
function classifyAccelerator(backend, device) {
|
|
1759
|
+
const b = backend.toLowerCase();
|
|
1760
|
+
const d = device?.toLowerCase() ?? null;
|
|
1761
|
+
if (b === "coreml" || b === "ane" || b === "mps") return "apple-ane";
|
|
1762
|
+
if (b === "openvino") {
|
|
1763
|
+
if (d === "npu") return "intel-npu";
|
|
1764
|
+
if (d === "cpu") return "cpu";
|
|
1765
|
+
return "intel-gpu";
|
|
1766
|
+
}
|
|
1767
|
+
if (b === "npu" || b === "intel-npu") return "intel-npu";
|
|
1768
|
+
if (b === "gpu" || b === "intel-gpu" || b === "gpu-openvino") return "intel-gpu";
|
|
1769
|
+
return "cpu";
|
|
1770
|
+
}
|
|
1771
|
+
/**
|
|
1772
|
+
* Resolve the hardware-aware default object-detection model id for a node's
|
|
1773
|
+
* engine `(backend, device, format)`.
|
|
1774
|
+
*
|
|
1775
|
+
* Guarantees:
|
|
1776
|
+
* - never throws (hot default-tree path);
|
|
1777
|
+
* - never returns a model id that is not in the object-detection catalog
|
|
1778
|
+
* with a build for `format` — an unmapped backend, a `'cpu'` class, a
|
|
1779
|
+
* mapped id missing from the catalog, or a mapped id without a `format`
|
|
1780
|
+
* build all fall back to the step's own `defaultModelId` (`yolo26n`).
|
|
1781
|
+
*
|
|
1782
|
+
* `getStepDef` is injectable (defaults to the real catalog lookup) so the
|
|
1783
|
+
* missing-build fallback branch is unit-testable without a live catalog —
|
|
1784
|
+
* mirrors `collectZeroBuildIssues`' injection seam.
|
|
1785
|
+
*/
|
|
1786
|
+
function resolveDefaultDetectionModel(backend, device, format, getStepDef = getStepDefinition) {
|
|
1787
|
+
let def;
|
|
1788
|
+
try {
|
|
1789
|
+
def = getStepDef(OBJECT_DETECTION_STEP_ID);
|
|
1790
|
+
} catch {
|
|
1791
|
+
return "yolo26n";
|
|
1792
|
+
}
|
|
1793
|
+
const fallback = def.defaultModelId;
|
|
1794
|
+
const candidate = MODEL_BY_CLASS[classifyAccelerator(backend, device)];
|
|
1795
|
+
if (candidate === null) return fallback;
|
|
1796
|
+
const entry = def.models.find((m) => m.id === candidate);
|
|
1797
|
+
if (!entry || entry.formats[format] === void 0) return fallback;
|
|
1798
|
+
return candidate;
|
|
1799
|
+
}
|
|
1800
|
+
//#endregion
|
|
1801
|
+
//#region src/detection-pipeline/engine/shared-inference-pool.ts
|
|
1802
|
+
/**
|
|
1803
|
+
* SharedInferencePool — TypeScript wrapper for inference_pool.py.
|
|
1804
|
+
*
|
|
1805
|
+
* Multi-process orchestrator. Owns N `PoolWorker` instances, each
|
|
1806
|
+
* holding its own Python subprocess + MLModel copy, and round-robins
|
|
1807
|
+
* inference dispatches across them. Model management (load/unload/
|
|
1808
|
+
* replace) propagates to all workers so every subprocess holds the
|
|
1809
|
+
* same model set at the same indices.
|
|
1810
|
+
*
|
|
1811
|
+
* Wire format (per worker):
|
|
1812
|
+
* Request: [4B total_len][4B req_id][1B msg_type][payload]
|
|
1813
|
+
* Response: [4B total_len][4B req_id][JSON payload]
|
|
1814
|
+
*/
|
|
1815
|
+
var MSG_COMMAND = 0;
|
|
1816
|
+
var MSG_INFER_JPEG = 1;
|
|
1817
|
+
var MSG_INFER_RAW = 2;
|
|
1818
|
+
var MSG_CACHE_FRAME = 4;
|
|
1819
|
+
var MSG_INFER_CACHED = 5;
|
|
1820
|
+
/**
|
|
1821
|
+
* Cross-camera batching opcode — N items packed into a single IPC
|
|
1822
|
+
* frame. Wire shape:
|
|
1823
|
+
* [1B model_idx][1B count][N × (4B w | 4B h | 1B fmt | 4B size | raw bytes)]
|
|
1824
|
+
* Python pool dispatches each item via `dispatcher.run` (asyncio.gather)
|
|
1825
|
+
* and returns a JSON array of detections in the same order.
|
|
1826
|
+
*/
|
|
1827
|
+
var MSG_INFER_BATCH = 3;
|
|
1828
|
+
var PREFIX_LEN = 9;
|
|
1829
|
+
/**
|
|
1830
|
+
* Wire-level enum for the raw-frame fast path. Values are append-only:
|
|
1831
|
+
* the Python pool reads the byte directly off the IPC frame; reordering
|
|
1832
|
+
* existing entries breaks all in-flight workers.
|
|
1833
|
+
*/
|
|
1834
|
+
var RAW_FMT_CODE = {
|
|
1835
|
+
rgb: 0,
|
|
1836
|
+
bgr: 1,
|
|
1837
|
+
gray: 2
|
|
1838
|
+
};
|
|
1839
|
+
var PoolHandle = class {
|
|
1840
|
+
pool;
|
|
1841
|
+
modelIndex;
|
|
1842
|
+
runtime = "onnx";
|
|
1843
|
+
device = "cpu";
|
|
1844
|
+
constructor(pool, modelIndex) {
|
|
1845
|
+
this.pool = pool;
|
|
1846
|
+
this.modelIndex = modelIndex;
|
|
1840
1847
|
}
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
inputSize: {
|
|
1848
|
-
width: 640,
|
|
1849
|
-
height: 640
|
|
1850
|
-
},
|
|
1851
|
-
labels: [],
|
|
1852
|
-
preprocessMode: "letterbox",
|
|
1853
|
-
formats: {
|
|
1854
|
-
onnx: {
|
|
1855
|
-
url: hf("segmentation/yolo26-seg/onnx/camstack-yolo26n-seg.onnx"),
|
|
1856
|
-
sizeMB: 11
|
|
1857
|
-
},
|
|
1858
|
-
coreml: {
|
|
1859
|
-
url: hf("segmentation/yolo26-seg/coreml/camstack-yolo26n-seg.mlpackage"),
|
|
1860
|
-
sizeMB: 6,
|
|
1861
|
-
isDirectory: true,
|
|
1862
|
-
files: [...MLPACKAGE_FILES],
|
|
1863
|
-
runtimes: ["python"]
|
|
1864
|
-
},
|
|
1865
|
-
openvino: ovFormat(hf("segmentation/yolo26-seg/openvino/camstack-yolo26n-seg.xml"), 11)
|
|
1866
|
-
}
|
|
1867
|
-
},
|
|
1868
|
-
{
|
|
1869
|
-
id: "yolo26s-seg",
|
|
1870
|
-
name: "YOLO26 Small Seg",
|
|
1871
|
-
description: "YOLO26 Small Segmentation — balanced instance segmentation",
|
|
1872
|
-
inputSize: {
|
|
1873
|
-
width: 640,
|
|
1874
|
-
height: 640
|
|
1875
|
-
},
|
|
1876
|
-
labels: [],
|
|
1877
|
-
preprocessMode: "letterbox",
|
|
1878
|
-
formats: {
|
|
1879
|
-
onnx: {
|
|
1880
|
-
url: hf("segmentation/yolo26-seg/onnx/camstack-yolo26s-seg.onnx"),
|
|
1881
|
-
sizeMB: 40
|
|
1882
|
-
},
|
|
1883
|
-
coreml: {
|
|
1884
|
-
url: hf("segmentation/yolo26-seg/coreml/camstack-yolo26s-seg.mlpackage"),
|
|
1885
|
-
sizeMB: 20,
|
|
1886
|
-
isDirectory: true,
|
|
1887
|
-
files: [...MLPACKAGE_FILES],
|
|
1888
|
-
runtimes: ["python"]
|
|
1889
|
-
},
|
|
1890
|
-
openvino: ovFormat(hf("segmentation/yolo26-seg/openvino/camstack-yolo26s-seg.xml"), 40)
|
|
1891
|
-
}
|
|
1892
|
-
},
|
|
1893
|
-
{
|
|
1894
|
-
id: "yolo26m-seg",
|
|
1895
|
-
name: "YOLO26 Medium Seg",
|
|
1896
|
-
description: "YOLO26 Medium Segmentation — high-accuracy instance segmentation",
|
|
1897
|
-
inputSize: {
|
|
1898
|
-
width: 640,
|
|
1899
|
-
height: 640
|
|
1900
|
-
},
|
|
1901
|
-
labels: [],
|
|
1902
|
-
preprocessMode: "letterbox",
|
|
1903
|
-
formats: {
|
|
1904
|
-
onnx: {
|
|
1905
|
-
url: hf("segmentation/yolo26-seg/onnx/camstack-yolo26m-seg.onnx"),
|
|
1906
|
-
sizeMB: 90
|
|
1907
|
-
},
|
|
1908
|
-
coreml: {
|
|
1909
|
-
url: hf("segmentation/yolo26-seg/coreml/camstack-yolo26m-seg.mlpackage"),
|
|
1910
|
-
sizeMB: 45,
|
|
1911
|
-
isDirectory: true,
|
|
1912
|
-
files: [...MLPACKAGE_FILES],
|
|
1913
|
-
runtimes: ["python"]
|
|
1914
|
-
},
|
|
1915
|
-
openvino: ovFormat(hf("segmentation/yolo26-seg/openvino/camstack-yolo26m-seg.xml"), 90)
|
|
1916
|
-
}
|
|
1848
|
+
async infer(input) {
|
|
1849
|
+
const start = performance.now();
|
|
1850
|
+
return {
|
|
1851
|
+
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),
|
|
1852
|
+
inferenceMs: performance.now() - start
|
|
1853
|
+
};
|
|
1917
1854
|
}
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1855
|
+
/**
|
|
1856
|
+
* Inference on a frame previously cached in the Python pool via
|
|
1857
|
+
* `pool.cacheFrame()`. Sends only 5 bytes through the pipe instead
|
|
1858
|
+
* of 1.2MB raw data — eliminates the pipe transfer bottleneck.
|
|
1859
|
+
*/
|
|
1860
|
+
async inferFromCache(frameId) {
|
|
1861
|
+
const start = performance.now();
|
|
1862
|
+
return {
|
|
1863
|
+
structured: await this.pool.inferCached(this.modelIndex, frameId),
|
|
1864
|
+
inferenceMs: performance.now() - start
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
async dispose() {}
|
|
1868
|
+
};
|
|
1869
|
+
/**
|
|
1870
|
+
* How long to wait for a worker to exit on SIGTERM before escalating to
|
|
1871
|
+
* SIGKILL. A worker idle between frames exits well under this; only a worker
|
|
1872
|
+
* stuck in a native inference call (openvino/onnx C++) needs the escalation.
|
|
1873
|
+
*/
|
|
1874
|
+
var POOL_WORKER_TERM_GRACE_MS = 2e3;
|
|
1875
|
+
/**
|
|
1876
|
+
* Terminate a spawned child GRACEFULLY THEN FORCEFULLY: end stdin, send
|
|
1877
|
+
* SIGTERM, and if the process hasn't exited within `graceMs`, send SIGKILL.
|
|
1878
|
+
* Resolves once the process has exited (or was already dead).
|
|
1879
|
+
*
|
|
1880
|
+
* The old teardown sent SIGTERM and dropped the reference immediately — a
|
|
1881
|
+
* worker busy in a native inference loop ignores SIGTERM and ORPHANS at full
|
|
1882
|
+
* CPU (observed after an engine re-spin). SIGKILL cannot be ignored, so the
|
|
1883
|
+
* escalation guarantees the process dies. Exported for tests.
|
|
1884
|
+
*/
|
|
1885
|
+
async function terminateChild(proc, graceMs) {
|
|
1886
|
+
if (proc.exitCode !== null || proc.signalCode !== null) return;
|
|
1887
|
+
try {
|
|
1888
|
+
proc.stdin?.end();
|
|
1889
|
+
} catch {}
|
|
1890
|
+
await new Promise((resolve) => {
|
|
1891
|
+
let settled = false;
|
|
1892
|
+
let timer = null;
|
|
1893
|
+
const done = () => {
|
|
1894
|
+
if (settled) return;
|
|
1895
|
+
settled = true;
|
|
1896
|
+
if (timer) clearTimeout(timer);
|
|
1897
|
+
resolve();
|
|
1898
|
+
};
|
|
1899
|
+
proc.once("exit", done);
|
|
1900
|
+
try {
|
|
1901
|
+
proc.kill("SIGTERM");
|
|
1902
|
+
} catch {
|
|
1903
|
+
done();
|
|
1904
|
+
return;
|
|
1947
1905
|
}
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1906
|
+
timer = setTimeout(() => {
|
|
1907
|
+
try {
|
|
1908
|
+
proc.kill("SIGKILL");
|
|
1909
|
+
} catch {}
|
|
1910
|
+
done();
|
|
1911
|
+
}, graceMs);
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
var PoolWorker = class {
|
|
1915
|
+
process = null;
|
|
1916
|
+
receiveBuffer = Buffer.alloc(0);
|
|
1917
|
+
pending = /* @__PURE__ */ new Map();
|
|
1918
|
+
nextRequestId = 1;
|
|
1919
|
+
ready = false;
|
|
1920
|
+
log;
|
|
1921
|
+
opts;
|
|
1922
|
+
constructor(opts) {
|
|
1923
|
+
this.opts = opts;
|
|
1924
|
+
this.log = opts.logger;
|
|
1925
|
+
}
|
|
1926
|
+
getPid() {
|
|
1927
|
+
return this.process?.pid ?? null;
|
|
1928
|
+
}
|
|
1929
|
+
isReady() {
|
|
1930
|
+
return this.ready;
|
|
1931
|
+
}
|
|
1932
|
+
async initialize(initialModels) {
|
|
1933
|
+
this.process = (0, node_child_process.spawn)(this.opts.pythonPath, [this.opts.scriptPath], { stdio: [
|
|
1934
|
+
"pipe",
|
|
1935
|
+
"pipe",
|
|
1936
|
+
"pipe"
|
|
1937
|
+
] });
|
|
1938
|
+
if (!this.process.stdout || !this.process.stdin) throw new Error("PoolWorker: failed to create process pipes");
|
|
1939
|
+
this.process.stderr?.on("data", (chunk) => {
|
|
1940
|
+
for (const line of chunk.toString().split("\n")) {
|
|
1941
|
+
const trimmed = line.trim();
|
|
1942
|
+
if (trimmed) this.log.warn(`[${this.opts.workerLabel}] ${trimmed}`);
|
|
1975
1943
|
}
|
|
1976
|
-
}
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
inputNormalization: "none",
|
|
1992
|
-
formats: {
|
|
1993
|
-
onnx: {
|
|
1994
|
-
url: hf("clip/mobileclip-s2/onnx/camstack-mobileclip-s2-vision.onnx"),
|
|
1995
|
-
sizeMB: 150
|
|
1996
|
-
},
|
|
1997
|
-
openvino: ovFormat(hf("clip/mobileclip-s2/openvino/camstack-mobileclip-s2-vision.xml"), 90),
|
|
1998
|
-
coreml: {
|
|
1999
|
-
url: hf("clip/mobileclip-s2/coreml/camstack-mobileclip-s2-vision.mlpackage"),
|
|
2000
|
-
sizeMB: 110,
|
|
2001
|
-
isDirectory: true,
|
|
2002
|
-
files: [...MLPACKAGE_FILES],
|
|
2003
|
-
runtimes: ["python"]
|
|
1944
|
+
});
|
|
1945
|
+
this.process.on("error", (err) => {
|
|
1946
|
+
this.log.error("Worker process error", { meta: {
|
|
1947
|
+
worker: this.opts.workerLabel,
|
|
1948
|
+
error: err.message
|
|
1949
|
+
} });
|
|
1950
|
+
this.rejectAll(err);
|
|
1951
|
+
});
|
|
1952
|
+
this.process.on("exit", (code) => {
|
|
1953
|
+
if (code !== 0 && code !== null) {
|
|
1954
|
+
this.log.error("Worker process exited", { meta: {
|
|
1955
|
+
worker: this.opts.workerLabel,
|
|
1956
|
+
code
|
|
1957
|
+
} });
|
|
1958
|
+
this.rejectAll(/* @__PURE__ */ new Error(`Worker process exited with code ${code}`));
|
|
2004
1959
|
}
|
|
1960
|
+
this.ready = false;
|
|
1961
|
+
});
|
|
1962
|
+
this.process.stdout.on("data", (chunk) => {
|
|
1963
|
+
this.receiveBuffer = Buffer.concat([this.receiveBuffer, chunk]);
|
|
1964
|
+
this.drainResponses();
|
|
1965
|
+
});
|
|
1966
|
+
const config = {
|
|
1967
|
+
runtime: this.opts.poolRuntime,
|
|
1968
|
+
concurrency: this.opts.concurrency,
|
|
1969
|
+
models: initialModels.map((m) => serializeModelConfig(m))
|
|
1970
|
+
};
|
|
1971
|
+
if (this.opts.tuning) {
|
|
1972
|
+
config["batch_mode"] = this.opts.tuning.batchMode;
|
|
1973
|
+
config["window_ms"] = this.opts.tuning.windowMs;
|
|
1974
|
+
config["max_batch_size"] = this.opts.tuning.maxBatchSize;
|
|
1975
|
+
config["num_streams"] = this.opts.tuning.numStreams;
|
|
1976
|
+
config["intra_op_threads"] = this.opts.tuning.intraOpThreads;
|
|
2005
1977
|
}
|
|
1978
|
+
const configBuf = Buffer.from(JSON.stringify(config), "utf8");
|
|
1979
|
+
this.writeFrame(0, MSG_COMMAND, configBuf);
|
|
1980
|
+
return new Promise((resolve, reject) => {
|
|
1981
|
+
const timeout = setTimeout(() => {
|
|
1982
|
+
this.pending.delete(0);
|
|
1983
|
+
reject(/* @__PURE__ */ new Error(`PoolWorker[${this.opts.workerLabel}]: timeout waiting for ready (120s)`));
|
|
1984
|
+
}, 12e4);
|
|
1985
|
+
this.pending.set(0, {
|
|
1986
|
+
resolve: (result) => {
|
|
1987
|
+
clearTimeout(timeout);
|
|
1988
|
+
if (result["status"] === "ready") {
|
|
1989
|
+
this.ready = true;
|
|
1990
|
+
const loadedCount = result["models"];
|
|
1991
|
+
const startupMs = result["startupMs"];
|
|
1992
|
+
resolve({
|
|
1993
|
+
startupMs,
|
|
1994
|
+
loadedCount,
|
|
1995
|
+
workers: result["workers"] ?? 1
|
|
1996
|
+
});
|
|
1997
|
+
} else reject(/* @__PURE__ */ new Error(`Unexpected pool status: ${JSON.stringify(result)}`));
|
|
1998
|
+
},
|
|
1999
|
+
reject: (err) => {
|
|
2000
|
+
clearTimeout(timeout);
|
|
2001
|
+
reject(err);
|
|
2002
|
+
}
|
|
2003
|
+
});
|
|
2004
|
+
});
|
|
2006
2005
|
}
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2006
|
+
async infer(modelByte, jpeg) {
|
|
2007
|
+
this.ensureReady();
|
|
2008
|
+
const payload = Buffer.concat([Buffer.from([modelByte]), jpeg]);
|
|
2009
|
+
return this.dispatch(MSG_INFER_JPEG, payload);
|
|
2010
|
+
}
|
|
2011
|
+
async inferRaw(modelByte, raw, width, height, format) {
|
|
2012
|
+
this.ensureReady();
|
|
2013
|
+
const fmtCode = RAW_FMT_CODE[format];
|
|
2014
|
+
const rawHeader = Buffer.allocUnsafe(10);
|
|
2015
|
+
rawHeader[0] = modelByte;
|
|
2016
|
+
rawHeader.writeUInt32LE(width, 1);
|
|
2017
|
+
rawHeader.writeUInt32LE(height, 5);
|
|
2018
|
+
rawHeader[9] = fmtCode;
|
|
2019
|
+
return this.dispatchMulti(MSG_INFER_RAW, rawHeader.length + raw.length, [rawHeader, raw]);
|
|
2020
|
+
}
|
|
2021
|
+
async inferBatch(modelByte, items) {
|
|
2022
|
+
this.ensureReady();
|
|
2023
|
+
if (items.length === 0) return [];
|
|
2024
|
+
const headerSize = 2 + items.length * 13;
|
|
2025
|
+
const totalRawSize = items.reduce((sum, it) => sum + it.raw.length, 0);
|
|
2026
|
+
const payload = Buffer.allocUnsafe(headerSize + totalRawSize);
|
|
2027
|
+
payload[0] = modelByte;
|
|
2028
|
+
payload[1] = items.length;
|
|
2029
|
+
let offset = 2;
|
|
2030
|
+
for (const item of items) {
|
|
2031
|
+
payload.writeUInt32LE(item.width, offset);
|
|
2032
|
+
payload.writeUInt32LE(item.height, offset + 4);
|
|
2033
|
+
payload[offset + 8] = RAW_FMT_CODE[item.format];
|
|
2034
|
+
payload.writeUInt32LE(item.raw.length, offset + 9);
|
|
2035
|
+
offset += 13;
|
|
2036
|
+
item.raw.copy(payload, offset);
|
|
2037
|
+
offset += item.raw.length;
|
|
2028
2038
|
}
|
|
2039
|
+
const response = await this.dispatch(MSG_INFER_BATCH, payload);
|
|
2040
|
+
const results = response["results"];
|
|
2041
|
+
if (!Array.isArray(results)) throw new Error(`PoolWorker.inferBatch: malformed response: ${JSON.stringify(response)}`);
|
|
2042
|
+
return results;
|
|
2029
2043
|
}
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
width
|
|
2036
|
-
height
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2044
|
+
async cacheFrame(frameId, raw, width, height, format) {
|
|
2045
|
+
this.ensureReady();
|
|
2046
|
+
const fmtCode = RAW_FMT_CODE[format];
|
|
2047
|
+
const header = Buffer.allocUnsafe(13);
|
|
2048
|
+
header.writeUInt32LE(frameId, 0);
|
|
2049
|
+
header.writeUInt32LE(width, 4);
|
|
2050
|
+
header.writeUInt32LE(height, 8);
|
|
2051
|
+
header[12] = fmtCode;
|
|
2052
|
+
const payloadLen = header.length + raw.length;
|
|
2053
|
+
const result = await this.dispatchMulti(MSG_CACHE_FRAME, payloadLen, [header, raw]);
|
|
2054
|
+
if (result["error"]) throw new Error(String(result["error"]));
|
|
2055
|
+
}
|
|
2056
|
+
async inferCached(modelByte, frameId) {
|
|
2057
|
+
this.ensureReady();
|
|
2058
|
+
const payload = Buffer.allocUnsafe(5);
|
|
2059
|
+
payload[0] = modelByte;
|
|
2060
|
+
payload.writeUInt32LE(frameId, 1);
|
|
2061
|
+
return this.dispatch(MSG_INFER_CACHED, payload);
|
|
2062
|
+
}
|
|
2063
|
+
async sendCommand(cmd) {
|
|
2064
|
+
this.ensureReady();
|
|
2065
|
+
const payload = Buffer.from(JSON.stringify(cmd), "utf8");
|
|
2066
|
+
return await this.dispatch(MSG_COMMAND, payload);
|
|
2067
|
+
}
|
|
2068
|
+
async dispose() {
|
|
2069
|
+
const proc = this.process;
|
|
2070
|
+
if (!proc) return;
|
|
2071
|
+
this.process = null;
|
|
2072
|
+
this.ready = false;
|
|
2073
|
+
await terminateChild(proc, POOL_WORKER_TERM_GRACE_MS);
|
|
2074
|
+
}
|
|
2075
|
+
dispatch(msgType, payload) {
|
|
2076
|
+
const reqId = this.allocRequestId();
|
|
2077
|
+
return new Promise((resolve, reject) => {
|
|
2078
|
+
this.pending.set(reqId, {
|
|
2079
|
+
resolve,
|
|
2080
|
+
reject
|
|
2081
|
+
});
|
|
2082
|
+
try {
|
|
2083
|
+
this.writeFrame(reqId, msgType, payload);
|
|
2084
|
+
} catch (err) {
|
|
2085
|
+
this.pending.delete(reqId);
|
|
2086
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
2087
|
+
}
|
|
2088
|
+
});
|
|
2089
|
+
}
|
|
2090
|
+
dispatchMulti(msgType, payloadLen, parts) {
|
|
2091
|
+
const reqId = this.allocRequestId();
|
|
2092
|
+
return new Promise((resolve, reject) => {
|
|
2093
|
+
this.pending.set(reqId, {
|
|
2094
|
+
resolve,
|
|
2095
|
+
reject
|
|
2096
|
+
});
|
|
2097
|
+
try {
|
|
2098
|
+
if (!this.process?.stdin) throw new Error("PoolWorker: not initialized");
|
|
2099
|
+
const prefix = Buffer.allocUnsafe(PREFIX_LEN);
|
|
2100
|
+
prefix.writeUInt32LE(5 + payloadLen, 0);
|
|
2101
|
+
prefix.writeUInt32LE(reqId, 4);
|
|
2102
|
+
prefix[8] = msgType;
|
|
2103
|
+
this.process.stdin.write(prefix);
|
|
2104
|
+
for (const part of parts) this.process.stdin.write(part);
|
|
2105
|
+
} catch (err) {
|
|
2106
|
+
this.pending.delete(reqId);
|
|
2107
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
2108
|
+
}
|
|
2109
|
+
});
|
|
2064
2110
|
}
|
|
2065
|
-
|
|
2066
|
-
|
|
2111
|
+
allocRequestId() {
|
|
2112
|
+
let id = this.nextRequestId;
|
|
2113
|
+
this.nextRequestId = id >= 4294967295 ? 1 : id + 1;
|
|
2114
|
+
while (this.pending.has(id)) {
|
|
2115
|
+
id = this.nextRequestId;
|
|
2116
|
+
this.nextRequestId = id >= 4294967295 ? 1 : id + 1;
|
|
2117
|
+
}
|
|
2118
|
+
return id;
|
|
2067
2119
|
}
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
options: [
|
|
2096
|
-
{
|
|
2097
|
-
value: "person",
|
|
2098
|
-
label: "Person"
|
|
2099
|
-
},
|
|
2100
|
-
{
|
|
2101
|
-
value: "vehicle",
|
|
2102
|
-
label: "Vehicle"
|
|
2103
|
-
},
|
|
2104
|
-
{
|
|
2105
|
-
value: "animal",
|
|
2106
|
-
label: "Animal"
|
|
2107
|
-
}
|
|
2108
|
-
],
|
|
2109
|
-
default: [
|
|
2110
|
-
"person",
|
|
2111
|
-
"vehicle",
|
|
2112
|
-
"animal"
|
|
2113
|
-
]
|
|
2114
|
-
},
|
|
2115
|
-
{
|
|
2116
|
-
type: "slider",
|
|
2117
|
-
key: "minConfidencePerson",
|
|
2118
|
-
label: "Min Confidence — Person",
|
|
2119
|
-
description: "Drop person predictions below this score.",
|
|
2120
|
-
min: 0,
|
|
2121
|
-
max: 1,
|
|
2122
|
-
step: .05,
|
|
2123
|
-
default: .5,
|
|
2124
|
-
showValue: true
|
|
2125
|
-
},
|
|
2126
|
-
{
|
|
2127
|
-
type: "slider",
|
|
2128
|
-
key: "minConfidenceVehicle",
|
|
2129
|
-
label: "Min Confidence — Vehicle",
|
|
2130
|
-
description: "Drop vehicle predictions below this score.",
|
|
2131
|
-
min: 0,
|
|
2132
|
-
max: 1,
|
|
2133
|
-
step: .05,
|
|
2134
|
-
default: .5,
|
|
2135
|
-
showValue: true
|
|
2136
|
-
},
|
|
2137
|
-
{
|
|
2138
|
-
type: "slider",
|
|
2139
|
-
key: "minConfidenceAnimal",
|
|
2140
|
-
label: "Min Confidence — Animal",
|
|
2141
|
-
description: "Drop animal predictions below this score.",
|
|
2142
|
-
min: 0,
|
|
2143
|
-
max: 1,
|
|
2144
|
-
step: .05,
|
|
2145
|
-
default: .5,
|
|
2146
|
-
showValue: true
|
|
2120
|
+
writeFrame(reqId, msgType, payload) {
|
|
2121
|
+
if (!this.process?.stdin) throw new Error("PoolWorker: not initialized");
|
|
2122
|
+
const totalLen = 5 + payload.length;
|
|
2123
|
+
const header = Buffer.allocUnsafe(PREFIX_LEN);
|
|
2124
|
+
header.writeUInt32LE(totalLen, 0);
|
|
2125
|
+
header.writeUInt32LE(reqId, 4);
|
|
2126
|
+
header[8] = msgType;
|
|
2127
|
+
this.process.stdin.write(header);
|
|
2128
|
+
this.process.stdin.write(payload);
|
|
2129
|
+
}
|
|
2130
|
+
ensureReady() {
|
|
2131
|
+
if (!this.ready || !this.process?.stdin) throw new Error(`PoolWorker[${this.opts.workerLabel}]: not initialized`);
|
|
2132
|
+
}
|
|
2133
|
+
drainResponses() {
|
|
2134
|
+
while (this.receiveBuffer.length >= 8) {
|
|
2135
|
+
const totalLen = this.receiveBuffer.readUInt32LE(0);
|
|
2136
|
+
if (this.receiveBuffer.length < 4 + totalLen) break;
|
|
2137
|
+
const reqId = this.receiveBuffer.readUInt32LE(4);
|
|
2138
|
+
const jsonBytes = this.receiveBuffer.subarray(8, 4 + totalLen);
|
|
2139
|
+
this.receiveBuffer = this.receiveBuffer.subarray(4 + totalLen);
|
|
2140
|
+
const entry = this.pending.get(reqId);
|
|
2141
|
+
if (!entry) {
|
|
2142
|
+
this.log.warn("Response for unknown request id", { meta: {
|
|
2143
|
+
worker: this.opts.workerLabel,
|
|
2144
|
+
reqId
|
|
2145
|
+
} });
|
|
2146
|
+
continue;
|
|
2147
2147
|
}
|
|
2148
|
-
|
|
2148
|
+
this.pending.delete(reqId);
|
|
2149
|
+
try {
|
|
2150
|
+
const parsed = JSON.parse(jsonBytes.toString("utf8"));
|
|
2151
|
+
entry.resolve(parsed);
|
|
2152
|
+
} catch (err) {
|
|
2153
|
+
entry.reject(err instanceof Error ? err : new Error(String(err)));
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
rejectAll(err) {
|
|
2158
|
+
const entries = [...this.pending.values()];
|
|
2159
|
+
this.pending.clear();
|
|
2160
|
+
for (const entry of entries) entry.reject(err);
|
|
2149
2161
|
}
|
|
2150
2162
|
};
|
|
2151
|
-
var
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2163
|
+
var SharedInferencePool = class {
|
|
2164
|
+
pythonPath;
|
|
2165
|
+
poolRuntime;
|
|
2166
|
+
workers = [];
|
|
2167
|
+
nextDispatchIdx = 0;
|
|
2168
|
+
nextFreeIndex = 0;
|
|
2169
|
+
nextFrameId = 1;
|
|
2170
|
+
/**
|
|
2171
|
+
* Cumulative count of frames the Python pool SHED under overload
|
|
2172
|
+
* (`"dropped": true` responses from the per-model in-flight bound in
|
|
2173
|
+
* inference_pool.py). Without this the shed response is
|
|
2174
|
+
* indistinguishable from a genuine "no detections" result.
|
|
2175
|
+
*/
|
|
2176
|
+
droppedResponseCount = 0;
|
|
2177
|
+
log;
|
|
2178
|
+
concurrency;
|
|
2179
|
+
tuning;
|
|
2180
|
+
numWorkers;
|
|
2181
|
+
constructor(pythonPath, poolRuntime, logger, options = {}) {
|
|
2182
|
+
this.pythonPath = pythonPath;
|
|
2183
|
+
this.poolRuntime = poolRuntime;
|
|
2184
|
+
this.log = logger;
|
|
2185
|
+
this.concurrency = Math.max(1, options.concurrency ?? 1);
|
|
2186
|
+
this.tuning = options.tuning ?? null;
|
|
2187
|
+
this.numWorkers = Math.max(1, options.numWorkers ?? 1);
|
|
2188
|
+
}
|
|
2189
|
+
/** Pid of the first worker (for legacy callers). Use `getPids()` for all. */
|
|
2190
|
+
getPid() {
|
|
2191
|
+
return this.workers[0]?.getPid() ?? null;
|
|
2192
|
+
}
|
|
2193
|
+
getPids() {
|
|
2194
|
+
return this.workers.map((w) => w.getPid());
|
|
2195
|
+
}
|
|
2196
|
+
/**
|
|
2197
|
+
* Start N pool workers in parallel. Each worker loads the same
|
|
2198
|
+
* initial model set so subsequent inference calls land on a worker
|
|
2199
|
+
* with the requested model already in its slot map.
|
|
2200
|
+
*/
|
|
2201
|
+
async initialize(initialModels = []) {
|
|
2202
|
+
const scriptPath = this.resolveScriptPath();
|
|
2203
|
+
for (let i = 0; i < this.numWorkers; i++) this.workers.push(new PoolWorker({
|
|
2204
|
+
pythonPath: this.pythonPath,
|
|
2205
|
+
scriptPath,
|
|
2206
|
+
poolRuntime: this.poolRuntime,
|
|
2207
|
+
concurrency: this.concurrency,
|
|
2208
|
+
tuning: this.tuning,
|
|
2209
|
+
logger: this.log,
|
|
2210
|
+
workerLabel: `w${i}`
|
|
2211
|
+
}));
|
|
2212
|
+
const t0 = performance.now();
|
|
2213
|
+
const results = await Promise.all(this.workers.map((w) => w.initialize(initialModels)));
|
|
2214
|
+
const startupMs = Math.round(performance.now() - t0);
|
|
2215
|
+
this.nextFreeIndex = initialModels.length;
|
|
2216
|
+
const loadedCount = results[0]?.loadedCount ?? 0;
|
|
2217
|
+
const innerWorkers = results.reduce((s, r) => s + r.workers, 0);
|
|
2218
|
+
this.log.info("Pool ready", { meta: {
|
|
2219
|
+
loadedCount,
|
|
2220
|
+
startupMs,
|
|
2221
|
+
runtime: this.poolRuntime,
|
|
2222
|
+
workers: this.numWorkers,
|
|
2223
|
+
pythonWorkers: innerWorkers,
|
|
2224
|
+
pids: this.getPids()
|
|
2225
|
+
} });
|
|
2226
|
+
return {
|
|
2227
|
+
startupMs,
|
|
2228
|
+
loadedCount,
|
|
2229
|
+
workers: this.numWorkers
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
2232
|
+
async infer(modelIndex, jpeg) {
|
|
2233
|
+
const w = this.pickWorker();
|
|
2234
|
+
return this.trackDroppedResponse(await w.infer(this.encodeModelByte(modelIndex), jpeg), modelIndex);
|
|
2198
2235
|
}
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
id: "animal-classifier",
|
|
2203
|
-
name: "Animal Classifier",
|
|
2204
|
-
slot: "classifier",
|
|
2205
|
-
postprocessor: "softmax",
|
|
2206
|
-
extractMode: "crop-roi",
|
|
2207
|
-
inputClasses: ["animal"],
|
|
2208
|
-
outputClasses: ["animal-type"],
|
|
2209
|
-
models: [...ANIMAL_CLASSIFIER_MODELS],
|
|
2210
|
-
defaultModelId: "animals-10",
|
|
2211
|
-
enabledByDefault: false,
|
|
2212
|
-
defaultConfidence: .2,
|
|
2213
|
-
labels: [...ANIMAL_10_LABELS]
|
|
2214
|
-
};
|
|
2215
|
-
getConfigSchema() {
|
|
2216
|
-
return [{
|
|
2217
|
-
type: "multiselect",
|
|
2218
|
-
key: "enabledClasses",
|
|
2219
|
-
label: "Species",
|
|
2220
|
-
description: "Which animal species to keep (empty = all). The classifier still scores every label but only the selected classes are returned on the detection.",
|
|
2221
|
-
options: ANIMAL_10_LABELS.map((l) => ({
|
|
2222
|
-
value: l,
|
|
2223
|
-
label: l
|
|
2224
|
-
})),
|
|
2225
|
-
default: []
|
|
2226
|
-
}, {
|
|
2227
|
-
type: "slider",
|
|
2228
|
-
key: "minConfidence",
|
|
2229
|
-
label: "Min Confidence",
|
|
2230
|
-
description: "Drop classifications whose score is below this threshold.",
|
|
2231
|
-
default: this.definition.defaultConfidence,
|
|
2232
|
-
min: 0,
|
|
2233
|
-
max: 1,
|
|
2234
|
-
step: .05,
|
|
2235
|
-
showValue: true
|
|
2236
|
-
}];
|
|
2236
|
+
async inferRaw(modelIndex, raw, width, height, format) {
|
|
2237
|
+
const w = this.pickWorker();
|
|
2238
|
+
return this.trackDroppedResponse(await w.inferRaw(this.encodeModelByte(modelIndex), raw, width, height, format), modelIndex);
|
|
2237
2239
|
}
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
return [{
|
|
2242
|
-
type: "slider",
|
|
2243
|
-
key: "minConfidence",
|
|
2244
|
-
label: "Min Confidence",
|
|
2245
|
-
description: "Drop classifications whose score is below this threshold.",
|
|
2246
|
-
default: this.definition.defaultConfidence,
|
|
2247
|
-
min: 0,
|
|
2248
|
-
max: 1,
|
|
2249
|
-
step: .05,
|
|
2250
|
-
showValue: true
|
|
2251
|
-
}];
|
|
2240
|
+
async inferBatch(modelIndex, items) {
|
|
2241
|
+
if (items.length > 255) throw new Error(`SharedInferencePool.inferBatch: max 255 items per call, got ${items.length}`);
|
|
2242
|
+
return this.pickWorker().inferBatch(this.encodeModelByte(modelIndex), items);
|
|
2252
2243
|
}
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
}
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
new
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
STEP_OBJECT_DETECTION_INSTANCE,
|
|
2381
|
-
STEP_FACE_DETECTION,
|
|
2382
|
-
STEP_FACE_EMBEDDING,
|
|
2383
|
-
STEP_CLIP_EMBEDDING,
|
|
2384
|
-
STEP_PLATE_DETECTION,
|
|
2385
|
-
STEP_PLATE_OCR,
|
|
2386
|
-
new AnimalClassifierStep(),
|
|
2387
|
-
STEP_BIRD_CLASSIFIER,
|
|
2388
|
-
STEP_VEHICLE_CLASSIFIER,
|
|
2389
|
-
STEP_SEGMENTATION,
|
|
2390
|
-
STEP_AUDIO_CLASSIFIER_INSTANCE
|
|
2391
|
-
];
|
|
2392
|
-
/** Compat: flat array of StepDefinition for existing consumers */
|
|
2393
|
-
var ALL_STEPS = ALL_PIPELINE_STEPS.map((s) => s.definition);
|
|
2394
|
-
var STEP_MAP = new Map(ALL_PIPELINE_STEPS.map((s) => [s.definition.id, s]));
|
|
2395
|
-
/**
|
|
2396
|
-
* Look up a pipeline step by ID (class instance with getConfigSchema).
|
|
2397
|
-
* @throws if the step ID is not registered.
|
|
2398
|
-
*/
|
|
2399
|
-
function getStep(stepId) {
|
|
2400
|
-
const step = STEP_MAP.get(stepId);
|
|
2401
|
-
if (!step) throw new Error(`Unknown pipeline step: "${stepId}". Available: ${ALL_PIPELINE_STEPS.map((s) => s.definition.id).join(", ")}`);
|
|
2402
|
-
return step;
|
|
2403
|
-
}
|
|
2404
|
-
/**
|
|
2405
|
-
* Look up a step definition by ID (compat shortcut).
|
|
2406
|
-
* @throws if the step ID is not registered.
|
|
2407
|
-
*/
|
|
2408
|
-
function getStepDefinition(stepId) {
|
|
2409
|
-
return getStep(stepId).definition;
|
|
2244
|
+
async inferCached(modelIndex, frameId) {
|
|
2245
|
+
const w = this.pickWorker();
|
|
2246
|
+
return this.trackDroppedResponse(await w.inferCached(this.encodeModelByte(modelIndex), frameId), modelIndex);
|
|
2247
|
+
}
|
|
2248
|
+
/**
|
|
2249
|
+
* Total `"dropped": true` responses seen from the Python pool — frames
|
|
2250
|
+
* shed by the per-model in-flight bound under overload. Diagnostic
|
|
2251
|
+
* counter (monotonic for the pool's lifetime).
|
|
2252
|
+
*/
|
|
2253
|
+
getDroppedResponseCount() {
|
|
2254
|
+
return this.droppedResponseCount;
|
|
2255
|
+
}
|
|
2256
|
+
getHandle(modelIndex) {
|
|
2257
|
+
return new PoolHandle(this, modelIndex);
|
|
2258
|
+
}
|
|
2259
|
+
async cacheFrame(raw, width, height, format) {
|
|
2260
|
+
const frameId = this.nextFrameId++;
|
|
2261
|
+
await Promise.all(this.workers.map((w) => w.cacheFrame(frameId, raw, width, height, format)));
|
|
2262
|
+
return {
|
|
2263
|
+
frameId,
|
|
2264
|
+
width,
|
|
2265
|
+
height
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
2268
|
+
async uncacheFrame(frameId) {
|
|
2269
|
+
await Promise.all(this.workers.map((w) => w.sendCommand({
|
|
2270
|
+
cmd: "uncache_frame",
|
|
2271
|
+
frameId
|
|
2272
|
+
})));
|
|
2273
|
+
}
|
|
2274
|
+
async loadModel(index, config) {
|
|
2275
|
+
const responses = await Promise.all(this.workers.map((w) => w.sendCommand({
|
|
2276
|
+
cmd: "load",
|
|
2277
|
+
index,
|
|
2278
|
+
config: serializeModelConfig(config)
|
|
2279
|
+
})));
|
|
2280
|
+
for (const resp of responses) if (resp.status !== "ok") throw new Error(`Failed to load model at index ${index}: ${resp.error ?? "unknown"}`);
|
|
2281
|
+
if (index >= this.nextFreeIndex) this.nextFreeIndex = index + 1;
|
|
2282
|
+
return { loadMs: Math.max(...responses.map((r) => r.loadMs ?? 0)) };
|
|
2283
|
+
}
|
|
2284
|
+
async unloadModel(index) {
|
|
2285
|
+
const responses = await Promise.all(this.workers.map((w) => w.sendCommand({
|
|
2286
|
+
cmd: "unload",
|
|
2287
|
+
index
|
|
2288
|
+
})));
|
|
2289
|
+
for (const resp of responses) if (resp.status !== "ok") throw new Error(`Failed to unload model at index ${index}: ${resp.error ?? "unknown"}`);
|
|
2290
|
+
}
|
|
2291
|
+
async replaceModel(index, config) {
|
|
2292
|
+
const responses = await Promise.all(this.workers.map((w) => w.sendCommand({
|
|
2293
|
+
cmd: "replace",
|
|
2294
|
+
index,
|
|
2295
|
+
config: serializeModelConfig(config)
|
|
2296
|
+
})));
|
|
2297
|
+
for (const resp of responses) if (resp.status !== "ok") throw new Error(`Failed to replace model at index ${index}: ${resp.error ?? "unknown"}`);
|
|
2298
|
+
return { loadMs: Math.max(...responses.map((r) => r.loadMs ?? 0)) };
|
|
2299
|
+
}
|
|
2300
|
+
async getStatus() {
|
|
2301
|
+
if (this.workers.length === 0) return [];
|
|
2302
|
+
return (await this.workers[0].sendCommand({ cmd: "status" })).models ?? [];
|
|
2303
|
+
}
|
|
2304
|
+
allocateIndex() {
|
|
2305
|
+
return this.nextFreeIndex++;
|
|
2306
|
+
}
|
|
2307
|
+
isReady() {
|
|
2308
|
+
return this.workers.length > 0 && this.workers.every((w) => w.isReady());
|
|
2309
|
+
}
|
|
2310
|
+
async dispose() {
|
|
2311
|
+
await Promise.all(this.workers.map((w) => w.dispose()));
|
|
2312
|
+
this.workers.length = 0;
|
|
2313
|
+
this.log.info("Pool process(es) terminated");
|
|
2314
|
+
}
|
|
2315
|
+
/**
|
|
2316
|
+
* Recognise a Python overload-shed response (`"dropped": true`) on the
|
|
2317
|
+
* single-frame inference paths. Previously indistinguishable from a
|
|
2318
|
+
* genuine empty detection result — now counted + debug-logged so
|
|
2319
|
+
* overload is measurable. The response passes through unchanged.
|
|
2320
|
+
*/
|
|
2321
|
+
trackDroppedResponse(result, modelIndex) {
|
|
2322
|
+
if (result["dropped"] === true) {
|
|
2323
|
+
this.droppedResponseCount++;
|
|
2324
|
+
this.log.debug("Python pool shed frame under overload", { meta: {
|
|
2325
|
+
modelIndex,
|
|
2326
|
+
droppedTotal: this.droppedResponseCount
|
|
2327
|
+
} });
|
|
2328
|
+
}
|
|
2329
|
+
return result;
|
|
2330
|
+
}
|
|
2331
|
+
pickWorker() {
|
|
2332
|
+
if (this.workers.length === 0) throw new Error("SharedInferencePool: not initialized");
|
|
2333
|
+
const idx = this.nextDispatchIdx;
|
|
2334
|
+
this.nextDispatchIdx = (this.nextDispatchIdx + 1) % this.workers.length;
|
|
2335
|
+
return this.workers[idx];
|
|
2336
|
+
}
|
|
2337
|
+
encodeModelByte(modelIndex) {
|
|
2338
|
+
if (modelIndex < 0 || modelIndex > 254) throw new Error(`Model index ${modelIndex} out of range (0-254)`);
|
|
2339
|
+
return modelIndex;
|
|
2340
|
+
}
|
|
2341
|
+
resolveScriptPath() {
|
|
2342
|
+
const candidates = [
|
|
2343
|
+
node_path.join(__dirname, "../../python/inference_pool.py"),
|
|
2344
|
+
node_path.join(__dirname, "../python/inference_pool.py"),
|
|
2345
|
+
node_path.join(__dirname, "../../../python/inference_pool.py")
|
|
2346
|
+
];
|
|
2347
|
+
try {
|
|
2348
|
+
const pkgPath = require.resolve("@camstack/addon-detection-pipeline/package.json");
|
|
2349
|
+
candidates.unshift(node_path.join(node_path.dirname(pkgPath), "python", "inference_pool.py"));
|
|
2350
|
+
} catch {}
|
|
2351
|
+
const found = candidates.find((p) => node_fs.existsSync(p));
|
|
2352
|
+
if (!found) throw new Error(`inference_pool.py not found. Searched:\n${candidates.join("\n")}`);
|
|
2353
|
+
return found;
|
|
2354
|
+
}
|
|
2355
|
+
};
|
|
2356
|
+
function serializeModelConfig(config) {
|
|
2357
|
+
const result = {
|
|
2358
|
+
path: config.path,
|
|
2359
|
+
inputSize: config.inputSize,
|
|
2360
|
+
preprocessMode: config.preprocessMode,
|
|
2361
|
+
postprocessor: config.postprocessor,
|
|
2362
|
+
confidence: config.confidence
|
|
2363
|
+
};
|
|
2364
|
+
if (config.labels) result["labels"] = config.labels;
|
|
2365
|
+
if (config.charset) result["charset"] = config.charset;
|
|
2366
|
+
if (config.numClasses) result["numClasses"] = config.numClasses;
|
|
2367
|
+
if (config.strides) result["strides"] = config.strides;
|
|
2368
|
+
if (config.maskThreshold !== void 0) result["maskThreshold"] = config.maskThreshold;
|
|
2369
|
+
if (config.device) result["device"] = config.device;
|
|
2370
|
+
return result;
|
|
2410
2371
|
}
|
|
2372
|
+
//#endregion
|
|
2373
|
+
//#region src/detection-pipeline/engine/step-utils.ts
|
|
2411
2374
|
/**
|
|
2412
|
-
*
|
|
2413
|
-
*
|
|
2414
|
-
*
|
|
2415
|
-
* 1. `def.defaultModelIdByFormat?.[format]` — the step's declared
|
|
2416
|
-
* per-format preference — if that model exists in `def.models` AND
|
|
2417
|
-
* has a build for `format`.
|
|
2418
|
-
* 2. `def.defaultModelId` — the step's plain declared default — if it
|
|
2419
|
-
* exists in `def.models` AND has a build for `format`.
|
|
2420
|
-
* 3. The smallest-by-size model among those with a `format` build
|
|
2421
|
-
* (legacy fallback, preserved for steps/formats with no declared
|
|
2422
|
-
* preference reachable).
|
|
2423
|
-
* 4. `def.defaultModelId` unchanged, when ZERO models have a `format`
|
|
2424
|
-
* build — an unloadable case flagged elsewhere, not resolved here.
|
|
2375
|
+
* Flatten a step tree into a list of enabled video steps (DFS).
|
|
2376
|
+
* Audio-classifier steps are excluded — they run independently
|
|
2377
|
+
* via the audio pipeline, not the video inference pool.
|
|
2425
2378
|
*/
|
|
2426
|
-
function
|
|
2427
|
-
const
|
|
2428
|
-
const
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2379
|
+
function flattenEnabledVideoSteps(steps) {
|
|
2380
|
+
const result = [];
|
|
2381
|
+
const walk = (nodes) => {
|
|
2382
|
+
for (const step of nodes) {
|
|
2383
|
+
if (!step.enabled) continue;
|
|
2384
|
+
if (step.slot === "audio-classifier") continue;
|
|
2385
|
+
result.push(step);
|
|
2386
|
+
if (step.children?.length) walk(step.children);
|
|
2387
|
+
}
|
|
2388
|
+
};
|
|
2389
|
+
walk(steps);
|
|
2390
|
+
return result;
|
|
2437
2391
|
}
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
}
|
|
2457
|
-
|
|
2392
|
+
//#endregion
|
|
2393
|
+
//#region src/detection-pipeline/engine/pipeline-model-manager.ts
|
|
2394
|
+
var PipelineModelManager = class {
|
|
2395
|
+
pool;
|
|
2396
|
+
buildPoolConfig;
|
|
2397
|
+
/** stepId → modelId → loaded entry. */
|
|
2398
|
+
loaded = /* @__PURE__ */ new Map();
|
|
2399
|
+
/** stepId → currently-active modelId (the runtime choice). */
|
|
2400
|
+
activeByStep = /* @__PURE__ */ new Map();
|
|
2401
|
+
/** Monotonic counter feeding the LRU tick on every touch. */
|
|
2402
|
+
lruClock = 0;
|
|
2403
|
+
log;
|
|
2404
|
+
maxModelsPerStep;
|
|
2405
|
+
constructor(pool, buildPoolConfig, logger, options) {
|
|
2406
|
+
this.pool = pool;
|
|
2407
|
+
this.buildPoolConfig = buildPoolConfig;
|
|
2408
|
+
this.log = logger;
|
|
2409
|
+
this.maxModelsPerStep = options?.maxModelsPerStep ?? 4;
|
|
2410
|
+
}
|
|
2411
|
+
/**
|
|
2412
|
+
* Apply a new pipeline configuration — driven by the runtime config
|
|
2413
|
+
* (camera-serving pipeline). Rewrites the *active* model per step,
|
|
2414
|
+
* keeping prior actives warm in extra slots up to `maxModelsPerStep`.
|
|
2415
|
+
*
|
|
2416
|
+
* Steps that disappear from `newSteps` get fully unloaded (every
|
|
2417
|
+
* model variant for that stepId).
|
|
2418
|
+
*/
|
|
2419
|
+
async applyConfig(newSteps) {
|
|
2420
|
+
const enabledSteps = flattenEnabledVideoSteps(newSteps);
|
|
2421
|
+
const diff = this.computeDiff(enabledSteps);
|
|
2422
|
+
for (const removed of diff.removed) {
|
|
2423
|
+
await this.unloadEntry(removed);
|
|
2424
|
+
this.log.info("Unloading step", { meta: {
|
|
2425
|
+
step: removed.stepId,
|
|
2426
|
+
modelId: removed.modelId,
|
|
2427
|
+
poolIndex: removed.poolIndex
|
|
2428
|
+
} });
|
|
2429
|
+
}
|
|
2430
|
+
for (const added of diff.added) {
|
|
2431
|
+
await this.ensureLoaded(added.addonId, added.modelId);
|
|
2432
|
+
this.activeByStep.set(added.addonId, added.modelId);
|
|
2433
|
+
this.log.info("Step loaded", { meta: {
|
|
2434
|
+
step: added.addonId,
|
|
2435
|
+
modelId: added.modelId,
|
|
2436
|
+
active: true
|
|
2437
|
+
} });
|
|
2438
|
+
}
|
|
2439
|
+
for (const { step, oldModelId } of diff.modelChanged) {
|
|
2440
|
+
await this.ensureLoaded(step.addonId, step.modelId);
|
|
2441
|
+
this.activeByStep.set(step.addonId, step.modelId);
|
|
2442
|
+
this.log.info("Active model switched", { meta: {
|
|
2443
|
+
step: step.addonId,
|
|
2444
|
+
fromModelId: oldModelId,
|
|
2445
|
+
toModelId: step.modelId
|
|
2446
|
+
} });
|
|
2447
|
+
await this.enforceLruCap(step.addonId);
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
/**
|
|
2451
|
+
* Get an `IInferenceEngine` handle. Without `modelId`, returns the
|
|
2452
|
+
* active model for the step (camera-runtime hot path). Pass
|
|
2453
|
+
* `modelId` to grab a specific warm variant (benchmark path).
|
|
2454
|
+
* Touches the LRU clock so the looked-up model survives eviction
|
|
2455
|
+
* pressure from concurrent loads.
|
|
2456
|
+
*/
|
|
2457
|
+
getHandle(stepId, modelId) {
|
|
2458
|
+
const entry = this.resolveOrThrow(stepId, modelId);
|
|
2459
|
+
this.touch(entry);
|
|
2460
|
+
return this.pool.getHandle(entry.poolIndex);
|
|
2461
|
+
}
|
|
2462
|
+
/** True iff the step has any model loaded. */
|
|
2463
|
+
isLoaded(stepId) {
|
|
2464
|
+
return this.loaded.has(stepId) && (this.loaded.get(stepId)?.size ?? 0) > 0;
|
|
2465
|
+
}
|
|
2466
|
+
/** True iff the specific (stepId, modelId) pair is resident. */
|
|
2467
|
+
isLoadedWithModel(stepId, modelId) {
|
|
2468
|
+
return this.loaded.get(stepId)?.has(modelId) ?? false;
|
|
2469
|
+
}
|
|
2470
|
+
/** ModelId currently designated as active for the step. */
|
|
2471
|
+
getLoadedModelId(stepId) {
|
|
2472
|
+
return this.activeByStep.get(stepId);
|
|
2473
|
+
}
|
|
2474
|
+
/**
|
|
2475
|
+
* Pool index for a given (stepId, modelId). Without `modelId`,
|
|
2476
|
+
* returns the active model's index. Used by the inference fast
|
|
2477
|
+
* paths that bypass `getHandle` and call `pool.inferBatch` directly.
|
|
2478
|
+
*/
|
|
2479
|
+
getPoolIndex(stepId, modelId) {
|
|
2480
|
+
const entry = this.resolve(stepId, modelId);
|
|
2481
|
+
if (!entry) return null;
|
|
2482
|
+
this.touch(entry);
|
|
2483
|
+
return entry.poolIndex;
|
|
2484
|
+
}
|
|
2485
|
+
/**
|
|
2486
|
+
* Direct access to the underlying pool — exposed so the provider can
|
|
2487
|
+
* call `inferBatch` without re-fetching individual handles. Pool
|
|
2488
|
+
* lifetime is owned by `EngineFactory`, so callers must not dispose.
|
|
2489
|
+
*/
|
|
2490
|
+
getPool() {
|
|
2491
|
+
return this.pool;
|
|
2492
|
+
}
|
|
2493
|
+
/**
|
|
2494
|
+
* Snapshot of every (stepId, modelId, poolIndex) currently resident.
|
|
2495
|
+
* Used by the `Warm instances` UI tab + cap-level `listLoadedEngines`
|
|
2496
|
+
* to surface both the runtime model and the warm extras.
|
|
2497
|
+
*/
|
|
2498
|
+
getLoadedSteps() {
|
|
2499
|
+
const out = [];
|
|
2500
|
+
for (const [stepId, models] of this.loaded) {
|
|
2501
|
+
const activeModelId = this.activeByStep.get(stepId);
|
|
2502
|
+
for (const entry of models.values()) out.push({
|
|
2503
|
+
stepId: entry.stepId,
|
|
2504
|
+
modelId: entry.modelId,
|
|
2505
|
+
poolIndex: entry.poolIndex,
|
|
2506
|
+
active: entry.modelId === activeModelId
|
|
2507
|
+
});
|
|
2508
|
+
}
|
|
2509
|
+
return out;
|
|
2458
2510
|
}
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2511
|
+
/**
|
|
2512
|
+
* Load extra models without touching the active runtime model.
|
|
2513
|
+
* Idempotent — already-loaded (stepId, modelId) pairs are no-op'd.
|
|
2514
|
+
* If `maxModelsPerStep` is exceeded, the LRU non-active variant is
|
|
2515
|
+
* evicted (NOT the active one — that would knock the runtime cameras
|
|
2516
|
+
* offline). The "active" designation is unchanged: callers without
|
|
2517
|
+
* a `modelId` argument keep hitting the runtime model.
|
|
2518
|
+
*/
|
|
2519
|
+
async loadAdditional(steps) {
|
|
2520
|
+
for (const step of steps) {
|
|
2521
|
+
if (this.isLoadedWithModel(step.addonId, step.modelId)) continue;
|
|
2522
|
+
await this.ensureLoaded(step.addonId, step.modelId);
|
|
2523
|
+
if (!this.activeByStep.has(step.addonId)) this.activeByStep.set(step.addonId, step.modelId);
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
/**
|
|
2527
|
+
* Load `(stepId, modelId)` into the pool if not already resident.
|
|
2528
|
+
* Honors `maxModelsPerStep` — evicts a non-active LRU variant first
|
|
2529
|
+
* when the cap would otherwise be exceeded.
|
|
2530
|
+
*/
|
|
2531
|
+
async ensureLoaded(stepId, modelId) {
|
|
2532
|
+
let perStep = this.loaded.get(stepId);
|
|
2533
|
+
if (!perStep) {
|
|
2534
|
+
perStep = /* @__PURE__ */ new Map();
|
|
2535
|
+
this.loaded.set(stepId, perStep);
|
|
2536
|
+
}
|
|
2537
|
+
const existing = perStep.get(modelId);
|
|
2538
|
+
if (existing) {
|
|
2539
|
+
this.touch(existing);
|
|
2540
|
+
return existing;
|
|
2541
|
+
}
|
|
2542
|
+
while (perStep.size >= this.maxModelsPerStep) {
|
|
2543
|
+
const evicted = this.pickEvictionTarget(stepId);
|
|
2544
|
+
if (!evicted) break;
|
|
2545
|
+
await this.unloadEntry(evicted);
|
|
2546
|
+
this.log.info("LRU evicted warm model", { meta: {
|
|
2547
|
+
step: stepId,
|
|
2548
|
+
modelId: evicted.modelId,
|
|
2549
|
+
poolIndex: evicted.poolIndex,
|
|
2550
|
+
cap: this.maxModelsPerStep
|
|
2551
|
+
} });
|
|
2552
|
+
}
|
|
2553
|
+
const index = this.pool.allocateIndex();
|
|
2554
|
+
const config = this.buildPoolConfig(stepId, modelId);
|
|
2555
|
+
this.log.info("Loading step variant", { meta: {
|
|
2556
|
+
step: stepId,
|
|
2557
|
+
modelId,
|
|
2558
|
+
poolIndex: index
|
|
2559
|
+
} });
|
|
2560
|
+
const { loadMs } = await this.pool.loadModel(index, config);
|
|
2561
|
+
this.log.info("Step variant loaded", { meta: {
|
|
2562
|
+
step: stepId,
|
|
2563
|
+
modelId,
|
|
2564
|
+
poolIndex: index,
|
|
2565
|
+
loadMs
|
|
2566
|
+
} });
|
|
2567
|
+
const entry = {
|
|
2568
|
+
stepId,
|
|
2569
|
+
modelId,
|
|
2570
|
+
poolIndex: index,
|
|
2571
|
+
lruTick: ++this.lruClock
|
|
2572
|
+
};
|
|
2573
|
+
perStep.set(modelId, entry);
|
|
2574
|
+
return entry;
|
|
2575
|
+
}
|
|
2576
|
+
async unloadEntry(entry) {
|
|
2577
|
+
await this.pool.unloadModel(entry.poolIndex);
|
|
2578
|
+
const perStep = this.loaded.get(entry.stepId);
|
|
2579
|
+
perStep?.delete(entry.modelId);
|
|
2580
|
+
if (perStep && perStep.size === 0) {
|
|
2581
|
+
this.loaded.delete(entry.stepId);
|
|
2582
|
+
this.activeByStep.delete(entry.stepId);
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
/** Select the LRU non-active entry for a step. Returns null when only the active one is present. */
|
|
2586
|
+
pickEvictionTarget(stepId) {
|
|
2587
|
+
const perStep = this.loaded.get(stepId);
|
|
2588
|
+
if (!perStep) return null;
|
|
2589
|
+
const activeModelId = this.activeByStep.get(stepId);
|
|
2590
|
+
let oldest = null;
|
|
2591
|
+
for (const entry of perStep.values()) {
|
|
2592
|
+
if (entry.modelId === activeModelId) continue;
|
|
2593
|
+
if (!oldest || entry.lruTick < oldest.lruTick) oldest = entry;
|
|
2594
|
+
}
|
|
2595
|
+
return oldest;
|
|
2596
|
+
}
|
|
2597
|
+
/** Trim the step down to `maxModelsPerStep` entries, evicting LRU non-actives. */
|
|
2598
|
+
async enforceLruCap(stepId) {
|
|
2599
|
+
const perStep = this.loaded.get(stepId);
|
|
2600
|
+
if (!perStep) return;
|
|
2601
|
+
while (perStep.size > this.maxModelsPerStep) {
|
|
2602
|
+
const evicted = this.pickEvictionTarget(stepId);
|
|
2603
|
+
if (!evicted) break;
|
|
2604
|
+
await this.unloadEntry(evicted);
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
resolve(stepId, modelId) {
|
|
2608
|
+
const perStep = this.loaded.get(stepId);
|
|
2609
|
+
if (!perStep) return null;
|
|
2610
|
+
const targetModelId = modelId ?? this.activeByStep.get(stepId);
|
|
2611
|
+
if (!targetModelId) return null;
|
|
2612
|
+
return perStep.get(targetModelId) ?? null;
|
|
2613
|
+
}
|
|
2614
|
+
resolveOrThrow(stepId, modelId) {
|
|
2615
|
+
const entry = this.resolve(stepId, modelId);
|
|
2616
|
+
if (!entry) throw new Error(`Step "${stepId}"${modelId ? ` (model "${modelId}")` : ""} is not loaded in the inference pool`);
|
|
2617
|
+
return entry;
|
|
2618
|
+
}
|
|
2619
|
+
touch(entry) {
|
|
2620
|
+
entry.lruTick = ++this.lruClock;
|
|
2621
|
+
}
|
|
2622
|
+
/** Compute diff between currently-active steps and desired steps. */
|
|
2623
|
+
computeDiff(desiredSteps) {
|
|
2624
|
+
const desiredMap = /* @__PURE__ */ new Map();
|
|
2625
|
+
for (const step of desiredSteps) desiredMap.set(step.addonId, step);
|
|
2626
|
+
const added = [];
|
|
2627
|
+
const removed = [];
|
|
2628
|
+
const modelChanged = [];
|
|
2629
|
+
const unchanged = [];
|
|
2630
|
+
for (const [stepId, activeModelId] of this.activeByStep) {
|
|
2631
|
+
const activeEntry = this.loaded.get(stepId)?.get(activeModelId);
|
|
2632
|
+
if (!activeEntry) continue;
|
|
2633
|
+
const desired = desiredMap.get(stepId);
|
|
2634
|
+
if (!desired) {
|
|
2635
|
+
const perStep = this.loaded.get(stepId);
|
|
2636
|
+
if (perStep) for (const entry of perStep.values()) removed.push(entry);
|
|
2637
|
+
} else if (desired.modelId !== activeModelId) modelChanged.push({
|
|
2638
|
+
step: desired,
|
|
2639
|
+
oldModelId: activeModelId
|
|
2640
|
+
});
|
|
2641
|
+
else unchanged.push(activeEntry);
|
|
2642
|
+
}
|
|
2643
|
+
for (const [stepId, step] of desiredMap) if (!this.activeByStep.has(stepId)) added.push(step);
|
|
2644
|
+
return {
|
|
2645
|
+
added,
|
|
2646
|
+
removed,
|
|
2647
|
+
modelChanged,
|
|
2648
|
+
unchanged
|
|
2649
|
+
};
|
|
2650
|
+
}
|
|
2651
|
+
};
|
|
2465
2652
|
//#endregion
|
|
2466
2653
|
//#region src/detection-pipeline/engine/engine-factory.ts
|
|
2467
2654
|
var BACKEND_TO_POOL_RUNTIME = {
|
|
@@ -4365,6 +4552,12 @@ function collectUnknownAddonIssues(steps, getStepDef = getStepDefinition) {
|
|
|
4365
4552
|
* `PipelineDefaultStep[]` (the catalog-enriched shape the executor consumes),
|
|
4366
4553
|
* resolving each step's model for `format` via `resolveModelForFormat`.
|
|
4367
4554
|
*
|
|
4555
|
+
* "Auto" model selection (ABSENT `modelId`) is hardware-aware for the
|
|
4556
|
+
* object-detection step when `engine` is provided: the default is resolved
|
|
4557
|
+
* via `resolveDefaultDetectionModel(engine.backend, engine.device, format)`.
|
|
4558
|
+
* A PINNED `modelId` is kept verbatim (subject to the usual format-build
|
|
4559
|
+
* substitution); every other step keeps its flat per-format default.
|
|
4560
|
+
*
|
|
4368
4561
|
* Pure and side-effect-free — never throws. This is the seam behind the
|
|
4369
4562
|
* LIVE per-camera dispatch path (`DetectionPipelineProvider.
|
|
4370
4563
|
* inputStepsToPipelineSteps`), which runs once per decoded frame (up to
|
|
@@ -4375,7 +4568,7 @@ function collectUnknownAddonIssues(steps, getStepDef = getStepDefinition) {
|
|
|
4375
4568
|
* `diagnostics.substitutions` instead of happening silently. The caller
|
|
4376
4569
|
* (the provider) owns logging + dedup for both.
|
|
4377
4570
|
*/
|
|
4378
|
-
function resolveInputSteps(steps, format) {
|
|
4571
|
+
function resolveInputSteps(steps, format, engine) {
|
|
4379
4572
|
const resolvedSteps = [];
|
|
4380
4573
|
const unknownAddonIds = [];
|
|
4381
4574
|
const substitutions = [];
|
|
@@ -4388,14 +4581,14 @@ function resolveInputSteps(steps, format) {
|
|
|
4388
4581
|
continue;
|
|
4389
4582
|
}
|
|
4390
4583
|
const chosenModelId = s.modelId ?? null;
|
|
4391
|
-
const runningModelId = resolveModelForFormat(s.addonId, chosenModelId, format);
|
|
4584
|
+
const runningModelId = chosenModelId === null && engine !== void 0 && s.addonId === "object-detection" ? resolveDefaultDetectionModel(engine.backend, engine.device, format) : resolveModelForFormat(s.addonId, chosenModelId, format);
|
|
4392
4585
|
if (chosenModelId !== null && runningModelId !== chosenModelId) substitutions.push({
|
|
4393
4586
|
addonId: s.addonId,
|
|
4394
4587
|
chosen: chosenModelId,
|
|
4395
4588
|
running: runningModelId,
|
|
4396
4589
|
format
|
|
4397
4590
|
});
|
|
4398
|
-
const childResult = s.children ? resolveInputSteps(s.children, format) : null;
|
|
4591
|
+
const childResult = s.children ? resolveInputSteps(s.children, format, engine) : null;
|
|
4399
4592
|
if (childResult) {
|
|
4400
4593
|
unknownAddonIds.push(...childResult.diagnostics.unknownAddonIds);
|
|
4401
4594
|
substitutions.push(...childResult.diagnostics.substitutions);
|
|
@@ -4428,8 +4621,6 @@ function resolveInputSteps(steps, format) {
|
|
|
4428
4621
|
* This is the main provider that consumers (DetectionWiring, Benchmark, tRPC)
|
|
4429
4622
|
* interact with. It manages the engine factory, pipeline executor, and config persistence.
|
|
4430
4623
|
*/
|
|
4431
|
-
var KEY_STEPS = "pipelineSteps";
|
|
4432
|
-
var KEY_ENGINE = "pipelineEngine";
|
|
4433
4624
|
var KEY_TEMPLATES = "pipelineTemplates";
|
|
4434
4625
|
function pythonModuleForBackend(backend) {
|
|
4435
4626
|
switch (backend) {
|
|
@@ -4512,14 +4703,6 @@ function readStored(raw, guard) {
|
|
|
4512
4703
|
return null;
|
|
4513
4704
|
}
|
|
4514
4705
|
}
|
|
4515
|
-
function isEngineChoice(v) {
|
|
4516
|
-
if (v === null || typeof v !== "object" || Array.isArray(v)) return false;
|
|
4517
|
-
const rec = { ...v };
|
|
4518
|
-
return typeof rec["runtime"] === "string" && typeof rec["backend"] === "string" && typeof rec["format"] === "string";
|
|
4519
|
-
}
|
|
4520
|
-
function isPipelineStepArray(v) {
|
|
4521
|
-
return Array.isArray(v);
|
|
4522
|
-
}
|
|
4523
4706
|
/**
|
|
4524
4707
|
* Phase 7 (settings redesign) template migration.
|
|
4525
4708
|
*
|
|
@@ -4736,13 +4919,6 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4736
4919
|
*/
|
|
4737
4920
|
frameHandleReaders = null;
|
|
4738
4921
|
/**
|
|
4739
|
-
* True once the engine + models are fully ready for inference. No
|
|
4740
|
-
* longer gates runtime dispatch (Phase 4 removed the legacy `runFrame`
|
|
4741
|
-
* which read this flag); kept as a diagnostic the admin UI / tests
|
|
4742
|
-
* surface via the future `isReady()` helper.
|
|
4743
|
-
*/
|
|
4744
|
-
ready = false;
|
|
4745
|
-
/**
|
|
4746
4922
|
* Lazy detection-engine runtime provisioner (Phase 2). Owns the
|
|
4747
4923
|
* idle → installing → verifying → ready state machine for the
|
|
4748
4924
|
* currently-selected engine and is the SOLE authority the inference
|
|
@@ -4794,6 +4970,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4794
4970
|
writeStore;
|
|
4795
4971
|
/** Read per-device settings. */
|
|
4796
4972
|
readDeviceStore;
|
|
4973
|
+
/** Delete all per-device settings for a device (used by `clearDeviceOverrides`). */
|
|
4974
|
+
clearDeviceStore;
|
|
4797
4975
|
constructor(settings, modelsDir, logger, eventBus = null, detectionConfigSchemaSource = null, executorOptions = {}) {
|
|
4798
4976
|
this.modelsDir = modelsDir;
|
|
4799
4977
|
this.eventBus = eventBus;
|
|
@@ -4803,6 +4981,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4803
4981
|
this.readStore = () => settings.readAddonStore();
|
|
4804
4982
|
this.writeStore = (patch) => settings.writeAddonStore(patch);
|
|
4805
4983
|
this.readDeviceStore = settings.readDeviceStore ?? (async () => ({}));
|
|
4984
|
+
const clearDeviceStore = settings.clearDeviceStore;
|
|
4985
|
+
this.clearDeviceStore = clearDeviceStore ? (deviceId) => clearDeviceStore.call(settings, deviceId) : async () => void 0;
|
|
4806
4986
|
this.currentEngine = ONNX_FLOOR;
|
|
4807
4987
|
this.log.info("Engine pick pending (placeholder until probe / persisted selection)", { meta: {
|
|
4808
4988
|
runtime: this.currentEngine.runtime,
|
|
@@ -4865,9 +5045,9 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4865
5045
|
}
|
|
4866
5046
|
await this.ensureEngineFactory();
|
|
4867
5047
|
}
|
|
4868
|
-
/** True when the engine
|
|
5048
|
+
/** True when the engine provisioning machine reports inference-ready. */
|
|
4869
5049
|
isReady() {
|
|
4870
|
-
return this.ready;
|
|
5050
|
+
return this.getEngineProvisioning().state === "ready";
|
|
4871
5051
|
}
|
|
4872
5052
|
/** Store the addon context. ctx.api is a lazy getter resolved at call time. */
|
|
4873
5053
|
async setApi(addonCtx) {
|
|
@@ -5269,7 +5449,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5269
5449
|
};
|
|
5270
5450
|
}
|
|
5271
5451
|
async getDefaultSteps(engine) {
|
|
5272
|
-
|
|
5452
|
+
const detectionModelId = resolveDefaultDetectionModel(engine.backend, engine.device ?? null, engine.format);
|
|
5453
|
+
return buildDefaultStepTree(engine.format, detectionModelId);
|
|
5273
5454
|
}
|
|
5274
5455
|
/**
|
|
5275
5456
|
* Resolve every step's model for this node's engine format via the shared
|
|
@@ -5312,45 +5493,25 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5312
5493
|
getActiveModelSubstitutions() {
|
|
5313
5494
|
return [...this.modelSubstitutions.values()];
|
|
5314
5495
|
}
|
|
5496
|
+
/**
|
|
5497
|
+
* PURE node-default step tree — computed on demand, never persisted.
|
|
5498
|
+
*
|
|
5499
|
+
* The object-detection default is hardware-aware (this node's resolved
|
|
5500
|
+
* backend + device via `resolveDefaultDetectionModel`); every other step
|
|
5501
|
+
* keeps its flat per-format default. The result is cached in
|
|
5502
|
+
* `currentSteps` only as a memo — the cache is dropped whenever the
|
|
5503
|
+
* engine (hence model format) changes, so a re-read always reflects the
|
|
5504
|
+
* live engine. Nothing in the live per-camera dispatch path reads this:
|
|
5505
|
+
* it exists as the benchmark fallback (empty input steps), the Gate-A
|
|
5506
|
+
* config-issue probe, and the "effective post-reset model" probe.
|
|
5507
|
+
*/
|
|
5315
5508
|
async getGlobalSteps() {
|
|
5316
5509
|
if (this.currentSteps) return this.currentSteps;
|
|
5317
|
-
const
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
this.writeStore({ [KEY_STEPS]: JSON.stringify(defaults) });
|
|
5323
|
-
this.log.info("Bootstrapped default pipeline — object-detection + face + plate recognition enabled by default", { meta: { rootSteps: defaults.length } });
|
|
5324
|
-
return this.currentSteps;
|
|
5325
|
-
}
|
|
5326
|
-
try {
|
|
5327
|
-
let steps = readStored(raw, isPipelineStepArray) ?? [];
|
|
5328
|
-
if (steps.length === 0) return null;
|
|
5329
|
-
if (!steps.some((s) => s.slot === "audio-classifier")) {
|
|
5330
|
-
const format = this.currentEngine.format;
|
|
5331
|
-
const audioDef = getStepDefinition("audio-classifier");
|
|
5332
|
-
if (audioDef.models.some((m) => m.formats[format])) {
|
|
5333
|
-
const modelId = getDefaultModelForFormat("audio-classifier", format);
|
|
5334
|
-
const audioStep = {
|
|
5335
|
-
addonId: audioDef.id,
|
|
5336
|
-
addonName: audioDef.name,
|
|
5337
|
-
slot: audioDef.slot,
|
|
5338
|
-
inputClasses: audioDef.inputClasses ?? [],
|
|
5339
|
-
outputClasses: audioDef.outputClasses,
|
|
5340
|
-
enabled: true,
|
|
5341
|
-
modelId,
|
|
5342
|
-
children: []
|
|
5343
|
-
};
|
|
5344
|
-
steps = [...steps, audioStep];
|
|
5345
|
-
this.writeStore({ [KEY_STEPS]: JSON.stringify(steps) });
|
|
5346
|
-
this.log.info("Migration: added audio-classifier step to persisted pipeline config");
|
|
5347
|
-
}
|
|
5348
|
-
}
|
|
5349
|
-
this.currentSteps = this.resolveStepModels(steps);
|
|
5350
|
-
return this.currentSteps;
|
|
5351
|
-
} catch {
|
|
5352
|
-
throw new Error(`Failed to parse persisted pipeline steps: corrupt data in key "${KEY_STEPS}"`);
|
|
5353
|
-
}
|
|
5510
|
+
const detectionModelId = resolveDefaultDetectionModel(this.currentEngine.backend, this.currentEngine.device ?? null, this.currentEngine.format);
|
|
5511
|
+
const defaults = buildDefaultStepTree(this.currentEngine.format, detectionModelId);
|
|
5512
|
+
if (defaults.length === 0) return null;
|
|
5513
|
+
this.currentSteps = this.resolveStepModels(defaults);
|
|
5514
|
+
return this.currentSteps;
|
|
5354
5515
|
}
|
|
5355
5516
|
async getGlobalPipelineConfig() {
|
|
5356
5517
|
if (!this.currentSteps) return null;
|
|
@@ -5805,7 +5966,14 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5805
5966
|
}
|
|
5806
5967
|
decodeMs = performance.now() - decodeT0;
|
|
5807
5968
|
const resolveFormat = input.engine?.format ?? this.currentEngine.format;
|
|
5808
|
-
const
|
|
5969
|
+
const resolveEngine = input.engine ? {
|
|
5970
|
+
backend: input.engine.backend,
|
|
5971
|
+
device: input.engine.device ?? null
|
|
5972
|
+
} : {
|
|
5973
|
+
backend: this.currentEngine.backend,
|
|
5974
|
+
device: this.currentEngine.device ?? null
|
|
5975
|
+
};
|
|
5976
|
+
const benchmarkSteps = this.inputStepsToPipelineSteps(input.steps, resolveFormat, resolveEngine);
|
|
5809
5977
|
const stepsSignature = JSON.stringify(benchmarkSteps.map((s) => ({
|
|
5810
5978
|
id: s.addonId,
|
|
5811
5979
|
settings: s.settings ?? {}
|
|
@@ -6118,8 +6286,11 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6118
6286
|
* would resolve models against the node's persisted format instead of the
|
|
6119
6287
|
* format actually being benchmarked.
|
|
6120
6288
|
*/
|
|
6121
|
-
inputStepsToPipelineSteps(steps, format = this.currentEngine.format
|
|
6122
|
-
|
|
6289
|
+
inputStepsToPipelineSteps(steps, format = this.currentEngine.format, engine = {
|
|
6290
|
+
backend: this.currentEngine.backend,
|
|
6291
|
+
device: this.currentEngine.device ?? null
|
|
6292
|
+
}) {
|
|
6293
|
+
const { steps: resolvedSteps, diagnostics } = resolveInputSteps(steps, format, engine);
|
|
6123
6294
|
for (const addonId of diagnostics.unknownAddonIds) this.logLiveDispatchIssueOnce(`unknown:${addonId}`, () => this.log.warn("Live pipeline step references an unknown addon — dropping step", { meta: { addonId } }));
|
|
6124
6295
|
for (const sub of diagnostics.substitutions) this.logLiveDispatchIssueOnce(`${sub.addonId}|${sub.chosen}|${sub.running}|${sub.format}`, () => this.log.info("Live pipeline step model substituted for engine format (node-local)", { meta: {
|
|
6125
6296
|
step: sub.addonId,
|
|
@@ -6237,67 +6408,6 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6237
6408
|
height: 640
|
|
6238
6409
|
};
|
|
6239
6410
|
}
|
|
6240
|
-
async detect(input) {
|
|
6241
|
-
const { addonId, frame } = input;
|
|
6242
|
-
const { executor, tree } = await this.ensureExecutor();
|
|
6243
|
-
const data = Buffer.from(frame.data);
|
|
6244
|
-
let rootInput;
|
|
6245
|
-
let jpegProvider;
|
|
6246
|
-
if (frame.format === "jpeg") {
|
|
6247
|
-
rootInput = {
|
|
6248
|
-
kind: "jpeg",
|
|
6249
|
-
data
|
|
6250
|
-
};
|
|
6251
|
-
jpegProvider = async () => data;
|
|
6252
|
-
} else if (frame.format === "rgb" || frame.format === "bgr" || frame.format === "gray") {
|
|
6253
|
-
const channels = frame.format === "gray" ? 1 : 3;
|
|
6254
|
-
const sharp$6 = (await import("sharp")).default;
|
|
6255
|
-
const jpeg = await sharp$6(data, { raw: {
|
|
6256
|
-
width: frame.width,
|
|
6257
|
-
height: frame.height,
|
|
6258
|
-
channels
|
|
6259
|
-
} }).jpeg({
|
|
6260
|
-
quality: 85,
|
|
6261
|
-
mozjpeg: false
|
|
6262
|
-
}).toBuffer();
|
|
6263
|
-
rootInput = {
|
|
6264
|
-
kind: "jpeg",
|
|
6265
|
-
data: jpeg
|
|
6266
|
-
};
|
|
6267
|
-
jpegProvider = async () => jpeg;
|
|
6268
|
-
} else {
|
|
6269
|
-
const sharp$7 = (await import("sharp")).default;
|
|
6270
|
-
const encoded = await sharp$7(data, { raw: {
|
|
6271
|
-
width: frame.width,
|
|
6272
|
-
height: frame.height,
|
|
6273
|
-
channels: 3
|
|
6274
|
-
} }).jpeg({
|
|
6275
|
-
quality: 80,
|
|
6276
|
-
mozjpeg: false
|
|
6277
|
-
}).toBuffer();
|
|
6278
|
-
rootInput = {
|
|
6279
|
-
kind: "jpeg",
|
|
6280
|
-
data: encoded
|
|
6281
|
-
};
|
|
6282
|
-
jpegProvider = async () => encoded;
|
|
6283
|
-
}
|
|
6284
|
-
const { result } = await executor.run(tree, rootInput, jpegProvider, frame.width, frame.height, 0);
|
|
6285
|
-
return {
|
|
6286
|
-
detections: result.detections.filter((d) => d.kind === "first-level").map((d) => ({
|
|
6287
|
-
class: d.macroClass,
|
|
6288
|
-
originalClass: d.debug?.originalClass ?? d.macroClass,
|
|
6289
|
-
score: d.score,
|
|
6290
|
-
bbox: {
|
|
6291
|
-
x: d.bbox.x,
|
|
6292
|
-
y: d.bbox.y,
|
|
6293
|
-
w: d.bbox.width,
|
|
6294
|
-
h: d.bbox.height
|
|
6295
|
-
}
|
|
6296
|
-
})),
|
|
6297
|
-
inferenceMs: result.debug?.totalInferenceMs ?? 0,
|
|
6298
|
-
modelId: addonId
|
|
6299
|
-
};
|
|
6300
|
-
}
|
|
6301
6411
|
/**
|
|
6302
6412
|
* Batched run — dispatches N raw frames against the same loaded
|
|
6303
6413
|
* single-step model in one IPC round-trip via
|
|
@@ -6340,7 +6450,14 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6340
6450
|
async runPipelineBatchImpl(input) {
|
|
6341
6451
|
if (input.frames.length === 0) return { results: [] };
|
|
6342
6452
|
const resolveFormat = input.engine?.format ?? this.currentEngine.format;
|
|
6343
|
-
const
|
|
6453
|
+
const resolveEngine = input.engine ? {
|
|
6454
|
+
backend: input.engine.backend,
|
|
6455
|
+
device: input.engine.device ?? null
|
|
6456
|
+
} : {
|
|
6457
|
+
backend: this.currentEngine.backend,
|
|
6458
|
+
device: this.currentEngine.device ?? null
|
|
6459
|
+
};
|
|
6460
|
+
const benchmarkSteps = this.inputStepsToPipelineSteps(input.steps, resolveFormat, resolveEngine);
|
|
6344
6461
|
const enabledSteps = flattenEnabledVideoSteps(benchmarkSteps);
|
|
6345
6462
|
if (enabledSteps.length === 0) throw new Error("runPipelineBatch: no enabled steps");
|
|
6346
6463
|
const rootStep = enabledSteps[0];
|
|
@@ -6632,71 +6749,11 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6632
6749
|
});
|
|
6633
6750
|
await this.engineFactory.initialize([]);
|
|
6634
6751
|
}
|
|
6635
|
-
async ensureExecutor() {
|
|
6636
|
-
if (this.initPromise) await this.initPromise;
|
|
6637
|
-
if (!this.engineFactory) {
|
|
6638
|
-
await this.assertEngineReady();
|
|
6639
|
-
this.initPromise = this.doInitialize();
|
|
6640
|
-
try {
|
|
6641
|
-
await this.initPromise;
|
|
6642
|
-
} finally {
|
|
6643
|
-
this.initPromise = null;
|
|
6644
|
-
}
|
|
6645
|
-
}
|
|
6646
|
-
if (!this.currentSteps) throw new Error("No pipeline steps configured");
|
|
6647
|
-
const tree = buildExecutableTree(this.currentSteps, (stepId) => this.engineFactory.getEngine(stepId));
|
|
6648
|
-
return {
|
|
6649
|
-
executor: this.executor,
|
|
6650
|
-
tree
|
|
6651
|
-
};
|
|
6652
|
-
}
|
|
6653
|
-
/** Actual initialization — download models, create engine, load pool. Called once. */
|
|
6654
|
-
async doInitialize() {
|
|
6655
|
-
const steps = await this.getGlobalSteps();
|
|
6656
|
-
if (!steps) throw new Error("No pipeline steps configured. Use setGlobalSteps() first.");
|
|
6657
|
-
this.log.info("Pipeline initializing — frames will be dropped until ready");
|
|
6658
|
-
const format = this.currentEngine.format;
|
|
6659
|
-
const downloads = [];
|
|
6660
|
-
for (const step of flattenSteps(steps)) {
|
|
6661
|
-
const modelEntry = await this.resolveModelEntry(step.addonId, step.modelId);
|
|
6662
|
-
if (!modelEntry) {
|
|
6663
|
-
this.log.warn("Model not found in step catalog — skipping download", { meta: {
|
|
6664
|
-
modelId: step.modelId,
|
|
6665
|
-
step: step.addonId
|
|
6666
|
-
} });
|
|
6667
|
-
continue;
|
|
6668
|
-
}
|
|
6669
|
-
if (!require_model_download_service_C_IHWnXx.isModelDownloaded(this.modelsDir, modelEntry, format)) this.log.info("Downloading model", { meta: {
|
|
6670
|
-
modelId: step.modelId,
|
|
6671
|
-
format
|
|
6672
|
-
} });
|
|
6673
|
-
downloads.push(require_model_download_service_C_IHWnXx.ensureModel(this.modelsDir, modelEntry, format).then(() => {}));
|
|
6674
|
-
}
|
|
6675
|
-
await Promise.all(downloads);
|
|
6676
|
-
await this.ensureBackendDeps(this.currentEngine);
|
|
6677
|
-
this.engineFactory = new EngineFactory({
|
|
6678
|
-
engine: this.currentEngine,
|
|
6679
|
-
modelsDir: this.modelsDir,
|
|
6680
|
-
logger: this.log.child("engine"),
|
|
6681
|
-
pythonPath: this.executorOptions.pythonPath ?? "",
|
|
6682
|
-
concurrency: this.executorOptions.concurrency,
|
|
6683
|
-
tuning: this.executorOptions.tuning,
|
|
6684
|
-
numWorkers: this.executorOptions.numWorkers
|
|
6685
|
-
});
|
|
6686
|
-
await this.engineFactory.initialize(steps);
|
|
6687
|
-
this.currentSteps = steps;
|
|
6688
|
-
const runtimeStr = `${this.currentEngine.runtime}+${this.currentEngine.backend}`;
|
|
6689
|
-
this.executor = new PipelineExecutor({
|
|
6690
|
-
engineRuntime: runtimeStr,
|
|
6691
|
-
logger: this.log
|
|
6692
|
-
});
|
|
6693
|
-
this.ready = true;
|
|
6694
|
-
this.log.info("Pipeline ready — accepting frames");
|
|
6695
|
-
}
|
|
6696
6752
|
/**
|
|
6697
6753
|
* Phase 2b — resolve the engine from the addon's new schema-backed
|
|
6698
|
-
* fields (`engineRuntime`, `engineBackend`, `engineDevice`).
|
|
6699
|
-
*
|
|
6754
|
+
* fields (`engineRuntime`, `engineBackend`, `engineDevice`). The legacy
|
|
6755
|
+
* `pipelineEngine` JSON-blob fallback was removed with the executor seed
|
|
6756
|
+
* (any store predating the per-node cascade simply re-runs auto-pick).
|
|
6700
6757
|
*
|
|
6701
6758
|
* The `'auto'` sentinel is treated as NO override (returns null) so the
|
|
6702
6759
|
* caller re-resolves LIVE via `resolveAutoEngine()` — `'auto'` is the
|
|
@@ -6739,8 +6796,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6739
6796
|
...device ? { device } : {}
|
|
6740
6797
|
};
|
|
6741
6798
|
}
|
|
6742
|
-
|
|
6743
|
-
return readStored(raw, isEngineChoice);
|
|
6799
|
+
return null;
|
|
6744
6800
|
}
|
|
6745
6801
|
/**
|
|
6746
6802
|
* Re-run the platform probe for the inference engine and persist the
|
|
@@ -6789,6 +6845,66 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6789
6845
|
this.log.info("videoPipelineSteps updated", { meta: { addons: Object.keys(sanitized).length } });
|
|
6790
6846
|
return { success: true };
|
|
6791
6847
|
}
|
|
6848
|
+
/**
|
|
6849
|
+
* Clear THIS node's executor-side PER-DEVICE settings stores — the
|
|
6850
|
+
* per-camera step overrides the object-detection root reads via
|
|
6851
|
+
* `applyDeviceOverridesToTree`.
|
|
6852
|
+
*
|
|
6853
|
+
* Node-routed (the cap's default `nodeIdMode: 'routing'`): the generated
|
|
6854
|
+
* cap-router strips `nodeId` and dispatches to the target node, so this
|
|
6855
|
+
* method runs ON the node being cleared and takes no argument.
|
|
6856
|
+
*
|
|
6857
|
+
* This is the slimmed executor leg of the orchestrator's
|
|
6858
|
+
* `resetNodePipelineDefaults` flow. The legacy `resetToDefault` — which
|
|
6859
|
+
* also reset a persisted GLOBAL step-tree seed nothing in the live
|
|
6860
|
+
* per-camera path read — was removed together with that seed
|
|
6861
|
+
* (`getGlobalSteps` is pure now, so there is nothing node-global left to
|
|
6862
|
+
* reset here).
|
|
6863
|
+
*
|
|
6864
|
+
* Idempotent: clearing already-empty per-device stores is a no-op.
|
|
6865
|
+
*/
|
|
6866
|
+
async clearDeviceOverrides() {
|
|
6867
|
+
const clearedDevices = await this.clearAllDeviceOverrides();
|
|
6868
|
+
this.log.info("executor per-device overrides cleared", { meta: { clearedDevices } });
|
|
6869
|
+
return {
|
|
6870
|
+
success: true,
|
|
6871
|
+
clearedDevices
|
|
6872
|
+
};
|
|
6873
|
+
}
|
|
6874
|
+
/**
|
|
6875
|
+
* Clear the per-device pipeline-step overrides for every known device.
|
|
6876
|
+
* Returns the count of devices whose store was cleared. There is no
|
|
6877
|
+
* bulk-list primitive over the per-device store, so devices are
|
|
6878
|
+
* enumerated via the device-manager and cleared one by one
|
|
6879
|
+
* (`clearDeviceStore` is idempotent — a device with no overrides is a
|
|
6880
|
+
* no-op). Never throws: a device-manager outage degrades to "cleared
|
|
6881
|
+
* nothing" so the reset flow still completes.
|
|
6882
|
+
*/
|
|
6883
|
+
async clearAllDeviceOverrides() {
|
|
6884
|
+
const api = this.addonCtx?.api;
|
|
6885
|
+
if (!api) return 0;
|
|
6886
|
+
let devices = [];
|
|
6887
|
+
try {
|
|
6888
|
+
devices = await api.deviceManager.listAll.query({});
|
|
6889
|
+
} catch (err) {
|
|
6890
|
+
this.log.warn("clearDeviceOverrides: device enumeration failed — skipping per-device clear", { meta: { error: require_dist.errMsg(err) } });
|
|
6891
|
+
return 0;
|
|
6892
|
+
}
|
|
6893
|
+
let cleared = 0;
|
|
6894
|
+
for (const device of devices) try {
|
|
6895
|
+
await this.clearDeviceStore(device.id);
|
|
6896
|
+
cleared++;
|
|
6897
|
+
} catch (err) {
|
|
6898
|
+
this.log.warn("clearDeviceOverrides: failed to clear per-device overrides for device", {
|
|
6899
|
+
tags: { deviceId: device.id },
|
|
6900
|
+
meta: {
|
|
6901
|
+
deviceId: device.id,
|
|
6902
|
+
error: require_dist.errMsg(err)
|
|
6903
|
+
}
|
|
6904
|
+
});
|
|
6905
|
+
}
|
|
6906
|
+
return cleared;
|
|
6907
|
+
}
|
|
6792
6908
|
async listLoadedEngines() {
|
|
6793
6909
|
const out = [];
|
|
6794
6910
|
const overrideFactory = this.overrideCache?.factory ?? null;
|
|
@@ -7114,7 +7230,7 @@ function resolveChildSlots(slot) {
|
|
|
7114
7230
|
* - Covers 90% of NVR use cases (person/vehicle/animal detection + face detection)
|
|
7115
7231
|
* - Users opt-in to heavy classifiers (vehicle type, plate OCR, bird species) when needed
|
|
7116
7232
|
*/
|
|
7117
|
-
function buildDefaultStepTree(format) {
|
|
7233
|
+
function buildDefaultStepTree(format, objectDetectionModelId) {
|
|
7118
7234
|
const makeStep = (stepId, children, opts) => {
|
|
7119
7235
|
const def = getStepDefinition(stepId);
|
|
7120
7236
|
if (!def.models.some((m) => m.formats[format])) return null;
|
|
@@ -7140,7 +7256,7 @@ function buildDefaultStepTree(format) {
|
|
|
7140
7256
|
makeStep("bird-classifier", [], { enabled: false }),
|
|
7141
7257
|
makeStep("vehicle-classifier", [], { enabled: false }),
|
|
7142
7258
|
makeStep("segmentation", [], { enabled: false })
|
|
7143
|
-
].filter((s) => s !== null));
|
|
7259
|
+
].filter((s) => s !== null), objectDetectionModelId !== void 0 ? { modelId: objectDetectionModelId } : void 0);
|
|
7144
7260
|
const audioStep = makeStep("audio-classifier", []);
|
|
7145
7261
|
const steps = [];
|
|
7146
7262
|
if (root) steps.push(root);
|