@camstack/addon-pipeline 1.1.44 → 1.1.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/dist/audio-analyzer/index.js +1 -1
  2. package/dist/audio-analyzer/index.mjs +1 -1
  3. package/dist/detection-pipeline/index.js +2330 -2061
  4. package/dist/detection-pipeline/index.mjs +2330 -2061
  5. package/dist/{dist-CySHUkXY.js → dist-CjxqjgEv.js} +39 -3
  6. package/dist/{dist-CgBGV988.mjs → dist-D04numuO.mjs} +39 -3
  7. package/dist/motion-wasm/index.js +1 -1
  8. package/dist/motion-wasm/index.mjs +1 -1
  9. package/dist/pipeline-runner/index.js +2 -2
  10. package/dist/pipeline-runner/index.mjs +2 -2
  11. package/dist/recorder/index.js +1 -1
  12. package/dist/recorder/index.mjs +1 -1
  13. package/dist/{remote-source-plane-D9m0z1N9.js → remote-source-plane-CLuxyDV2.js} +1 -1
  14. package/dist/{remote-source-plane-B4hRwchu.mjs → remote-source-plane-D31IILFN.mjs} +1 -1
  15. package/dist/stream-broker/_stub.js +2 -2
  16. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-G6wtUb3G.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BGKueo9D.mjs} +2 -2
  17. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Dlagq9X_.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D0sKyXiy.mjs} +1 -1
  18. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BUpF966l.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-8BUVCFIR.mjs} +1 -1
  19. package/dist/stream-broker/{hostInit-BSzP3BIj.mjs → hostInit-DazV2RCq.mjs} +2 -2
  20. package/dist/stream-broker/index.js +2 -2
  21. package/dist/stream-broker/index.mjs +2 -2
  22. package/dist/stream-broker/remoteEntry.js +1 -1
  23. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-D2ML5TPi.js → MaskShapeCanvas-DI4BY7W2-D_F4a_3H.js} +1 -1
  24. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-TTWGGaqv.js → MotionZonesSettings-NcxxQN8r-CfTBrn4G.js} +1 -1
  25. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-CJqHU2nU.js → PrivacyMaskSettings-APgPLF7p-BWYkeoRj.js} +1 -1
  26. package/embed-dist/assets/{index-C1oaL9s7.js → index-_ccEdNW7.js} +5 -5
  27. package/embed-dist/index.html +1 -1
  28. package/package.json +1 -1
@@ -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-CySHUkXY.js");
6
+ const require_dist = require("../dist-CjxqjgEv.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,2170 +298,2357 @@ function projectNodeEngine(store, nodeId) {
298
298
  return out;
299
299
  }
300
300
  //#endregion
301
- //#region src/detection-pipeline/engine/shared-inference-pool.ts
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
- * SharedInferencePool TypeScript wrapper for inference_pool.py.
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
- * Wire format (per worker):
312
- * Request: [4B total_len][4B req_id][1B msg_type][payload]
313
- * Response: [4B total_len][4B req_id][JSON payload]
314
- */
315
- var MSG_COMMAND = 0;
316
- var MSG_INFER_JPEG = 1;
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 RAW_FMT_CODE = {
335
- rgb: 0,
336
- bgr: 1,
337
- gray: 2
338
- };
339
- var PoolHandle = class {
340
- pool;
341
- modelIndex;
342
- runtime = "onnx";
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
- * How long to wait for a worker to exit on SIGTERM before escalating to
371
- * SIGKILL. A worker idle between frames exits well under this; only a worker
372
- * stuck in a native inference call (openvino/onnx C++) needs the escalation.
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 POOL_WORKER_TERM_GRACE_MS = 2e3;
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
- * Terminate a spawned child GRACEFULLY THEN FORCEFULLY: end stdin, send
377
- * SIGTERM, and if the process hasn't exited within `graceMs`, send SIGKILL.
378
- * Resolves once the process has exited (or was already dead).
379
- *
380
- * The old teardown sent SIGTERM and dropped the reference immediately — a
381
- * worker busy in a native inference loop ignores SIGTERM and ORPHANS at full
382
- * CPU (observed after an engine re-spin). SIGKILL cannot be ignored, so the
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
- async function terminateChild(proc, graceMs) {
386
- if (proc.exitCode !== null || proc.signalCode !== null) return;
387
- try {
388
- proc.stdin?.end();
389
- } catch {}
390
- await new Promise((resolve) => {
391
- let settled = false;
392
- let timer = null;
393
- const done = () => {
394
- if (settled) return;
395
- settled = true;
396
- if (timer) clearTimeout(timer);
397
- resolve();
398
- };
399
- proc.once("exit", done);
400
- try {
401
- proc.kill("SIGTERM");
402
- } catch {
403
- done();
404
- return;
405
- }
406
- timer = setTimeout(() => {
407
- try {
408
- proc.kill("SIGKILL");
409
- } catch {}
410
- done();
411
- }, graceMs);
412
- });
413
- }
414
- var PoolWorker = class {
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
- const configBuf = Buffer.from(JSON.stringify(config), "utf8");
479
- this.writeFrame(0, MSG_COMMAND, configBuf);
480
- return new Promise((resolve, reject) => {
481
- const timeout = setTimeout(() => {
482
- this.pending.delete(0);
483
- reject(/* @__PURE__ */ new Error(`PoolWorker[${this.opts.workerLabel}]: timeout waiting for ready (120s)`));
484
- }, 12e4);
485
- this.pending.set(0, {
486
- resolve: (result) => {
487
- clearTimeout(timeout);
488
- if (result["status"] === "ready") {
489
- this.ready = true;
490
- const loadedCount = result["models"];
491
- const startupMs = result["startupMs"];
492
- resolve({
493
- startupMs,
494
- loadedCount,
495
- workers: result["workers"] ?? 1
496
- });
497
- } else reject(/* @__PURE__ */ new Error(`Unexpected pool status: ${JSON.stringify(result)}`));
498
- },
499
- reject: (err) => {
500
- clearTimeout(timeout);
501
- reject(err);
502
- }
503
- });
504
- });
505
- }
506
- async infer(modelByte, jpeg) {
507
- this.ensureReady();
508
- const payload = Buffer.concat([Buffer.from([modelByte]), jpeg]);
509
- return this.dispatch(MSG_INFER_JPEG, payload);
510
- }
511
- async inferRaw(modelByte, raw, width, height, format) {
512
- this.ensureReady();
513
- const fmtCode = RAW_FMT_CODE[format];
514
- const rawHeader = Buffer.allocUnsafe(10);
515
- rawHeader[0] = modelByte;
516
- rawHeader.writeUInt32LE(width, 1);
517
- rawHeader.writeUInt32LE(height, 5);
518
- rawHeader[9] = fmtCode;
519
- return this.dispatchMulti(MSG_INFER_RAW, rawHeader.length + raw.length, [rawHeader, raw]);
520
- }
521
- async inferBatch(modelByte, items) {
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
- const response = await this.dispatch(MSG_INFER_BATCH, payload);
540
- const results = response["results"];
541
- if (!Array.isArray(results)) throw new Error(`PoolWorker.inferBatch: malformed response: ${JSON.stringify(response)}`);
542
- return results;
543
- }
544
- async cacheFrame(frameId, raw, width, height, format) {
545
- this.ensureReady();
546
- const fmtCode = RAW_FMT_CODE[format];
547
- const header = Buffer.allocUnsafe(13);
548
- header.writeUInt32LE(frameId, 0);
549
- header.writeUInt32LE(width, 4);
550
- header.writeUInt32LE(height, 8);
551
- header[12] = fmtCode;
552
- const payloadLen = header.length + raw.length;
553
- const result = await this.dispatchMulti(MSG_CACHE_FRAME, payloadLen, [header, raw]);
554
- if (result["error"]) throw new Error(String(result["error"]));
555
- }
556
- async inferCached(modelByte, frameId) {
557
- this.ensureReady();
558
- const payload = Buffer.allocUnsafe(5);
559
- payload[0] = modelByte;
560
- payload.writeUInt32LE(frameId, 1);
561
- return this.dispatch(MSG_INFER_CACHED, payload);
562
- }
563
- async sendCommand(cmd) {
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
- return id;
619
- }
620
- writeFrame(reqId, msgType, payload) {
621
- if (!this.process?.stdin) throw new Error("PoolWorker: not initialized");
622
- const totalLen = 5 + payload.length;
623
- const header = Buffer.allocUnsafe(PREFIX_LEN);
624
- header.writeUInt32LE(totalLen, 0);
625
- header.writeUInt32LE(reqId, 4);
626
- header[8] = msgType;
627
- this.process.stdin.write(header);
628
- this.process.stdin.write(payload);
629
- }
630
- ensureReady() {
631
- if (!this.ready || !this.process?.stdin) throw new Error(`PoolWorker[${this.opts.workerLabel}]: not initialized`);
632
- }
633
- drainResponses() {
634
- while (this.receiveBuffer.length >= 8) {
635
- const totalLen = this.receiveBuffer.readUInt32LE(0);
636
- if (this.receiveBuffer.length < 4 + totalLen) break;
637
- const reqId = this.receiveBuffer.readUInt32LE(4);
638
- const jsonBytes = this.receiveBuffer.subarray(8, 4 + totalLen);
639
- this.receiveBuffer = this.receiveBuffer.subarray(4 + totalLen);
640
- const entry = this.pending.get(reqId);
641
- if (!entry) {
642
- this.log.warn("Response for unknown request id", { meta: {
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
- rejectAll(err) {
658
- const entries = [...this.pending.values()];
659
- this.pending.clear();
660
- for (const entry of entries) entry.reject(err);
661
- }
662
- };
663
- var SharedInferencePool = class {
664
- pythonPath;
665
- poolRuntime;
666
- workers = [];
667
- nextDispatchIdx = 0;
668
- nextFreeIndex = 0;
669
- nextFrameId = 1;
670
- /**
671
- * Cumulative count of frames the Python pool SHED under overload
672
- * (`"dropped": true` responses from the per-model in-flight bound in
673
- * inference_pool.py). Without this the shed response is
674
- * indistinguishable from a genuine "no detections" result.
675
- */
676
- droppedResponseCount = 0;
677
- log;
678
- concurrency;
679
- tuning;
680
- numWorkers;
681
- constructor(pythonPath, poolRuntime, logger, options = {}) {
682
- this.pythonPath = pythonPath;
683
- this.poolRuntime = poolRuntime;
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);
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)
887
528
  }
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
- } });
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)
929
557
  }
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
- } });
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)
938
586
  }
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);
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)
948
615
  }
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
- });
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)
1008
641
  }
1009
- return out;
1010
- }
1011
- /**
1012
- * Load extra models without touching the active runtime model.
1013
- * Idempotent already-loaded (stepId, modelId) pairs are no-op'd.
1014
- * If `maxModelsPerStep` is exceeded, the LRU non-active variant is
1015
- * evicted (NOT the active one — that would knock the runtime cameras
1016
- * offline). The "active" designation is unchanged: callers without
1017
- * a `modelId` argument keep hitting the runtime model.
1018
- */
1019
- async loadAdditional(steps) {
1020
- for (const step of steps) {
1021
- if (this.isLoadedWithModel(step.addonId, step.modelId)) continue;
1022
- await this.ensureLoaded(step.addonId, step.modelId);
1023
- if (!this.activeByStep.has(step.addonId)) this.activeByStep.set(step.addonId, step.modelId);
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)
1024
667
  }
1025
- }
1026
- /**
1027
- * Load `(stepId, modelId)` into the pool if not already resident.
1028
- * Honors `maxModelsPerStep` — evicts a non-active LRU variant first
1029
- * when the cap would otherwise be exceeded.
1030
- */
1031
- async ensureLoaded(stepId, modelId) {
1032
- let perStep = this.loaded.get(stepId);
1033
- if (!perStep) {
1034
- perStep = /* @__PURE__ */ new Map();
1035
- this.loaded.set(stepId, perStep);
1036
- }
1037
- const existing = perStep.get(modelId);
1038
- if (existing) {
1039
- this.touch(existing);
1040
- return existing;
1041
- }
1042
- while (perStep.size >= this.maxModelsPerStep) {
1043
- const evicted = this.pickEvictionTarget(stepId);
1044
- if (!evicted) break;
1045
- await this.unloadEntry(evicted);
1046
- this.log.info("LRU evicted warm model", { meta: {
1047
- step: stepId,
1048
- modelId: evicted.modelId,
1049
- poolIndex: evicted.poolIndex,
1050
- cap: this.maxModelsPerStep
1051
- } });
1052
- }
1053
- const index = this.pool.allocateIndex();
1054
- const config = this.buildPoolConfig(stepId, modelId);
1055
- this.log.info("Loading step variant", { meta: {
1056
- step: stepId,
1057
- modelId,
1058
- poolIndex: index
1059
- } });
1060
- const { loadMs } = await this.pool.loadModel(index, config);
1061
- this.log.info("Step variant loaded", { meta: {
1062
- step: stepId,
1063
- modelId,
1064
- poolIndex: index,
1065
- loadMs
1066
- } });
1067
- const entry = {
1068
- stepId,
1069
- modelId,
1070
- poolIndex: index,
1071
- lruTick: ++this.lruClock
1072
- };
1073
- perStep.set(modelId, entry);
1074
- return entry;
1075
- }
1076
- async unloadEntry(entry) {
1077
- await this.pool.unloadModel(entry.poolIndex);
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);
1083
- }
1084
- }
1085
- /** Select the LRU non-active entry for a step. Returns null when only the active one is present. */
1086
- pickEvictionTarget(stepId) {
1087
- const perStep = this.loaded.get(stepId);
1088
- if (!perStep) return null;
1089
- const activeModelId = this.activeByStep.get(stepId);
1090
- let oldest = null;
1091
- for (const entry of perStep.values()) {
1092
- if (entry.modelId === activeModelId) continue;
1093
- if (!oldest || entry.lruTick < oldest.lruTick) oldest = entry;
1094
- }
1095
- return oldest;
1096
- }
1097
- /** Trim the step down to `maxModelsPerStep` entries, evicting LRU non-actives. */
1098
- async enforceLruCap(stepId) {
1099
- const perStep = this.loaded.get(stepId);
1100
- if (!perStep) return;
1101
- while (perStep.size > this.maxModelsPerStep) {
1102
- const evicted = this.pickEvictionTarget(stepId);
1103
- if (!evicted) break;
1104
- await this.unloadEntry(evicted);
1105
- }
1106
- }
1107
- resolve(stepId, modelId) {
1108
- const perStep = this.loaded.get(stepId);
1109
- if (!perStep) return null;
1110
- const targetModelId = modelId ?? this.activeByStep.get(stepId);
1111
- if (!targetModelId) return null;
1112
- return perStep.get(targetModelId) ?? null;
1113
- }
1114
- resolveOrThrow(stepId, modelId) {
1115
- const entry = this.resolve(stepId, modelId);
1116
- if (!entry) throw new Error(`Step "${stepId}"${modelId ? ` (model "${modelId}")` : ""} is not loaded in the inference pool`);
1117
- return entry;
1118
- }
1119
- touch(entry) {
1120
- entry.lruTick = ++this.lruClock;
1121
- }
1122
- /** Compute diff between currently-active steps and desired steps. */
1123
- computeDiff(desiredSteps) {
1124
- const desiredMap = /* @__PURE__ */ new Map();
1125
- for (const step of desiredSteps) desiredMap.set(step.addonId, step);
1126
- const added = [];
1127
- const removed = [];
1128
- const modelChanged = [];
1129
- const unchanged = [];
1130
- for (const [stepId, activeModelId] of this.activeByStep) {
1131
- const activeEntry = this.loaded.get(stepId)?.get(activeModelId);
1132
- if (!activeEntry) continue;
1133
- const desired = desiredMap.get(stepId);
1134
- if (!desired) {
1135
- const perStep = this.loaded.get(stepId);
1136
- if (perStep) for (const entry of perStep.values()) removed.push(entry);
1137
- } else if (desired.modelId !== activeModelId) modelChanged.push({
1138
- step: desired,
1139
- oldModelId: activeModelId
1140
- });
1141
- else unchanged.push(activeEntry);
1142
- }
1143
- for (const [stepId, step] of desiredMap) if (!this.activeByStep.has(stepId)) added.push(step);
1144
- return {
1145
- added,
1146
- removed,
1147
- modelChanged,
1148
- unchanged
1149
- };
1150
- }
1151
- };
1152
- //#endregion
1153
- //#region src/detection-pipeline/registry/model-catalogs.ts
1154
- var HF_REPO = "camstack/camstack-models";
1155
- var HF_SCRYPTED = "scrypted/plugin-models";
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`,
1190
- inputSize: {
1191
- width: 640,
1192
- height: 640
1193
668
  },
1194
- labels: [],
1195
- preprocessMode: "letterbox",
1196
- formats: { openvino: ovFormat(hf(`${ovDir}/camstack-${baseId}-${precision}.xml`), sizeMB) },
1197
- ...legacy ? { legacy: true } : {},
1198
- ...precision === "int8" ? { group: {
1199
- family: baseId.slice(0, -1),
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
669
  {
1211
- id: "yolov9t",
670
+ id: "scrypted-yolov9s-relu",
1212
671
  legacy: true,
1213
- name: "YOLOv9 Tiny",
1214
- description: "YOLOv9 Tinyultra-lightweight next-gen detector",
672
+ name: "YOLOv9 Small ReLU",
673
+ description: "YOLOv9s ReLUimproved efficiency, int8 ready",
1215
674
  inputSize: {
1216
- width: 640,
1217
- height: 640
675
+ width: 320,
676
+ height: 320
1218
677
  },
1219
678
  labels: [],
1220
679
  preprocessMode: "letterbox",
1221
680
  formats: {
1222
681
  onnx: {
1223
- url: hf("objectDetection/yolov9/onnx/camstack-yolov9t.onnx"),
1224
- sizeMB: 8
682
+ url: hfScrypted("onnx/scrypted_yolov9s_relu/scrypted_yolov9s_relu.onnx"),
683
+ sizeMB: 27.9
1225
684
  },
1226
685
  coreml: {
1227
- url: hf("objectDetection/yolov9/coreml/camstack-yolov9t.mlpackage"),
1228
- sizeMB: 4,
686
+ url: hfScrypted("coreml/scrypted_yolov9s_relu/scrypted_yolov9s_relu.mlpackage"),
687
+ sizeMB: 14,
1229
688
  isDirectory: true,
1230
689
  files: [...MLPACKAGE_FILES],
1231
690
  runtimes: ["python"]
1232
691
  },
1233
- openvino: ovFormat(hf("objectDetection/yolov9/openvino/camstack-yolov9t.xml"), 6)
692
+ openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9s_relu.xml"), 16)
1234
693
  }
1235
694
  },
1236
695
  {
1237
- id: "yolov9s",
696
+ id: "scrypted-yolov9c-relu",
1238
697
  legacy: true,
1239
- name: "YOLOv9 Small",
1240
- description: "YOLOv9 Smallimproved efficiency over YOLOv8s",
698
+ name: "YOLOv9 C ReLU",
699
+ description: "YOLOv9c ReLUhigh-accuracy, int8 ready",
1241
700
  inputSize: {
1242
- width: 640,
1243
- height: 640
701
+ width: 320,
702
+ height: 320
1244
703
  },
1245
704
  labels: [],
1246
705
  preprocessMode: "letterbox",
1247
706
  formats: {
1248
707
  onnx: {
1249
- url: hf("objectDetection/yolov9/onnx/camstack-yolov9s.onnx"),
1250
- sizeMB: 28
708
+ url: hfScrypted("onnx/scrypted_yolov9c_relu/scrypted_yolov9c_relu.onnx"),
709
+ sizeMB: 96.9
1251
710
  },
1252
711
  coreml: {
1253
- url: hf("objectDetection/yolov9/coreml/camstack-yolov9s.mlpackage"),
1254
- sizeMB: 14,
712
+ url: hfScrypted("coreml/scrypted_yolov9c_relu/scrypted_yolov9c_relu.mlpackage"),
713
+ sizeMB: 48,
1255
714
  isDirectory: true,
1256
715
  files: [...MLPACKAGE_FILES],
1257
716
  runtimes: ["python"]
1258
717
  },
1259
- openvino: ovFormat(hf("objectDetection/yolov9/openvino/camstack-yolov9s.xml"), 16)
718
+ openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9c_relu.xml"), 49)
1260
719
  }
1261
720
  },
1262
721
  {
1263
- id: "yolov9c",
722
+ id: "scrypted-yolov9m-relu",
1264
723
  legacy: true,
1265
- name: "YOLOv9 C",
1266
- description: "YOLOv9 Chigh-accuracy compact model",
724
+ name: "YOLOv9 M ReLU",
725
+ description: "YOLOv9m ReLUmedium, int8 ready",
1267
726
  inputSize: {
1268
- width: 640,
1269
- height: 640
727
+ width: 320,
728
+ height: 320
1270
729
  },
1271
730
  labels: [],
1272
731
  preprocessMode: "letterbox",
1273
732
  formats: {
1274
733
  onnx: {
1275
- url: hf("objectDetection/yolov9/onnx/camstack-yolov9c.onnx"),
1276
- sizeMB: 97
734
+ url: hfScrypted("onnx/scrypted_yolov9m_relu/scrypted_yolov9m_relu.onnx"),
735
+ sizeMB: 76.6
1277
736
  },
1278
737
  coreml: {
1279
- url: hf("objectDetection/yolov9/coreml/camstack-yolov9c.mlpackage"),
1280
- sizeMB: 48,
738
+ url: hfScrypted("coreml/scrypted_yolov9m_relu/scrypted_yolov9m_relu.mlpackage"),
739
+ sizeMB: 38,
1281
740
  isDirectory: true,
1282
741
  files: [...MLPACKAGE_FILES],
1283
742
  runtimes: ["python"]
1284
743
  },
1285
- openvino: ovFormat(hf("objectDetection/yolov9/openvino/camstack-yolov9c.xml"), 49)
744
+ openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9m_relu.xml"), 38)
1286
745
  }
1287
746
  },
1288
- {
1289
- id: "yolo26n",
1290
- group: {
1291
- family: "yolo26",
1292
- tier: "n"
1293
- },
1294
- name: "YOLO26 Nano",
1295
- description: "YOLO26 Nano — latest generation ultra-lightweight detector with attention mechanism",
1296
- inputSize: {
1297
- width: 640,
1298
- height: 640
1299
- },
1300
- labels: [],
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
- }
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
1316
817
  },
1317
- {
1318
- id: "yolo26s",
1319
- group: {
1320
- family: "yolo26",
1321
- tier: "s"
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
1322
827
  },
1323
- name: "YOLO26 Small",
1324
- description: "YOLO26 Small — balanced speed/accuracy with attention-based architecture",
1325
- inputSize: {
1326
- width: 640,
1327
- height: 640
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"]
1328
834
  },
1329
- labels: [],
1330
- preprocessMode: "letterbox",
1331
- formats: {
1332
- onnx: {
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
- }
835
+ openvino: ovFormat(hf("faceDetection/scrfd/openvino/camstack-scrfd-2.5g.xml"), 1.8)
836
+ }
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
1345
844
  },
1346
- {
1347
- id: "yolo26m",
1348
- group: {
1349
- family: "yolo26",
1350
- tier: "m"
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
1351
854
  },
1352
- name: "YOLO26 Medium",
1353
- description: "YOLO26 Medium — high accuracy general-purpose detector",
1354
- inputSize: {
1355
- width: 640,
1356
- height: 640
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"]
1357
861
  },
1358
- labels: [],
1359
- preprocessMode: "letterbox",
1360
- formats: {
1361
- onnx: {
1362
- url: hf("objectDetection/yolo26/onnx/camstack-yolo26m.onnx"),
1363
- sizeMB: 78
1364
- },
1365
- coreml: {
1366
- url: hf("objectDetection/yolo26/coreml/camstack-yolo26m.mlpackage"),
1367
- sizeMB: 39,
1368
- isDirectory: true,
1369
- files: [...MLPACKAGE_FILES],
1370
- runtimes: ["python"]
1371
- },
1372
- openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26m.xml"), 78)
1373
- }
862
+ openvino: ovFormat(hf("faceDetection/scrypted-yolov9-face/openvino/scrypted_yolov9t_relu_face.xml"), 6)
863
+ }
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
1374
872
  },
1375
- {
1376
- id: "yolo26l",
1377
- group: {
1378
- family: "yolo26",
1379
- tier: "l"
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
1380
884
  },
1381
- name: "YOLO26 Large",
1382
- description: "YOLO26 Large — maximum accuracy for demanding deployments",
1383
- inputSize: {
1384
- width: 640,
1385
- height: 640
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"]
1386
891
  },
1387
- labels: [],
1388
- preprocessMode: "letterbox",
1389
- formats: {
1390
- onnx: {
1391
- url: hf("objectDetection/yolo26/onnx/camstack-yolo26l.onnx"),
1392
- sizeMB: 95
1393
- },
1394
- coreml: {
1395
- url: hf("objectDetection/yolo26/coreml/camstack-yolo26l.mlpackage"),
1396
- sizeMB: 48,
1397
- isDirectory: true,
1398
- files: [...MLPACKAGE_FILES],
1399
- runtimes: ["python"]
1400
- },
1401
- openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26l.xml"), 95)
1402
- }
892
+ openvino: ovFormat(hf("faceRecognition/arcface/openvino/camstack-arcface-r100.xml"), 65)
893
+ }
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
1403
901
  },
1404
- {
1405
- id: "yolo26x",
1406
- legacy: true,
1407
- name: "YOLO26 XLarge",
1408
- description: "YOLO26 XLarge — highest accuracy, attention-based architecture",
1409
- inputSize: {
1410
- width: 640,
1411
- height: 640
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
1412
911
  },
1413
- labels: [],
1414
- preprocessMode: "letterbox",
1415
- formats: {
1416
- onnx: {
1417
- url: hf("objectDetection/yolo26/onnx/camstack-yolo26x.onnx"),
1418
- sizeMB: 213
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)
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)
920
+ }
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",
926
+ inputSize: {
927
+ width: 640,
928
+ height: 640
929
+ },
930
+ labels: [{
931
+ id: "plate",
932
+ name: "License Plate"
933
+ }],
934
+ preprocessMode: "letterbox",
935
+ formats: {
936
+ onnx: {
937
+ url: hf("plateDetection/yolov8-plate/onnx/camstack-yolov8n-plate.onnx"),
938
+ sizeMB: 12
939
+ },
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
957
+ },
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
967
+ },
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
985
+ },
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
996
+ },
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
1014
+ },
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
1059
+ },
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"]
1066
+ },
1067
+ openvino: ovFormat(hf("vehicleClassification/efficientnet/openvino/camstack-vehicle-type-efficientnet.xml"), 68)
1068
+ },
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
1092
+ },
1093
+ coreml: {
1094
+ url: hf("segmentationRefiner/u2netp/coreml/camstack-u2netp.mlpackage"),
1095
+ sizeMB: 3,
1096
+ isDirectory: true,
1097
+ files: [...MLPACKAGE_FILES],
1098
+ runtimes: ["python"]
1099
+ },
1100
+ openvino: ovFormat(hf("segmentationRefiner/u2netp/openvino/camstack-u2netp.xml"), 2.5)
1101
+ }
1102
+ }];
1103
+ var INSTANCE_SEGMENTATION_MODELS = [
1104
+ {
1105
+ id: "yolo26n-seg",
1106
+ name: "YOLO26 Nano Seg",
1107
+ description: "YOLO26 Nano Segmentation — ultra-lightweight instance segmentation with masks",
1108
+ inputSize: {
1109
+ width: 640,
1110
+ height: 640
1111
+ },
1112
+ labels: [],
1113
+ preprocessMode: "letterbox",
1114
+ formats: {
1115
+ onnx: {
1116
+ url: hf("segmentation/yolo26-seg/onnx/camstack-yolo26n-seg.onnx"),
1117
+ sizeMB: 11
1118
+ },
1119
+ coreml: {
1120
+ url: hf("segmentation/yolo26-seg/coreml/camstack-yolo26n-seg.mlpackage"),
1121
+ sizeMB: 6,
1122
+ isDirectory: true,
1123
+ files: [...MLPACKAGE_FILES],
1124
+ runtimes: ["python"]
1125
+ },
1126
+ openvino: ovFormat(hf("segmentation/yolo26-seg/openvino/camstack-yolo26n-seg.xml"), 11)
1127
+ }
1128
+ },
1129
+ {
1130
+ id: "yolo26s-seg",
1131
+ name: "YOLO26 Small Seg",
1132
+ description: "YOLO26 Small Segmentation — balanced instance segmentation",
1133
+ inputSize: {
1134
+ width: 640,
1135
+ height: 640
1136
+ },
1137
+ labels: [],
1138
+ preprocessMode: "letterbox",
1139
+ formats: {
1140
+ onnx: {
1141
+ url: hf("segmentation/yolo26-seg/onnx/camstack-yolo26s-seg.onnx"),
1142
+ sizeMB: 40
1143
+ },
1144
+ coreml: {
1145
+ url: hf("segmentation/yolo26-seg/coreml/camstack-yolo26s-seg.mlpackage"),
1146
+ sizeMB: 20,
1147
+ isDirectory: true,
1148
+ files: [...MLPACKAGE_FILES],
1149
+ runtimes: ["python"]
1150
+ },
1151
+ openvino: ovFormat(hf("segmentation/yolo26-seg/openvino/camstack-yolo26s-seg.xml"), 40)
1152
+ }
1153
+ },
1154
+ {
1155
+ id: "yolo26m-seg",
1156
+ name: "YOLO26 Medium Seg",
1157
+ description: "YOLO26 Medium Segmentation — high-accuracy instance segmentation",
1158
+ inputSize: {
1159
+ width: 640,
1160
+ height: 640
1161
+ },
1162
+ labels: [],
1163
+ preprocessMode: "letterbox",
1164
+ formats: {
1165
+ onnx: {
1166
+ url: hf("segmentation/yolo26-seg/onnx/camstack-yolo26m-seg.onnx"),
1167
+ sizeMB: 90
1168
+ },
1169
+ coreml: {
1170
+ url: hf("segmentation/yolo26-seg/coreml/camstack-yolo26m-seg.mlpackage"),
1171
+ sizeMB: 45,
1172
+ isDirectory: true,
1173
+ files: [...MLPACKAGE_FILES],
1174
+ runtimes: ["python"]
1175
+ },
1176
+ openvino: ovFormat(hf("segmentation/yolo26-seg/openvino/camstack-yolo26m-seg.xml"), 90)
1177
+ }
1178
+ }
1179
+ ];
1180
+ var CLIP_EMBEDDING_MODELS = [
1181
+ {
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)",
1185
+ inputSize: {
1186
+ width: 256,
1187
+ height: 256
1188
+ },
1189
+ labels: [{
1190
+ id: "embedding",
1191
+ name: "CLIP Embedding"
1192
+ }],
1193
+ preprocessMode: "resize",
1194
+ inputNormalization: "none",
1195
+ formats: {
1196
+ onnx: {
1197
+ url: hf("clip/mobileclip-s0/onnx/camstack-mobileclip-s0-vision.onnx"),
1198
+ sizeMB: 40
1199
+ },
1200
+ openvino: ovFormat(hf("clip/mobileclip-s0/openvino/camstack-mobileclip-s0-vision.xml"), 25),
1201
+ coreml: {
1202
+ url: hf("clip/mobileclip-s0/coreml/camstack-mobileclip-s0-vision.mlpackage"),
1203
+ sizeMB: 30,
1204
+ isDirectory: true,
1205
+ files: [...MLPACKAGE_FILES],
1206
+ runtimes: ["python"]
1207
+ }
1208
+ }
1209
+ },
1210
+ {
1211
+ id: "mobileclip-s1",
1212
+ name: "MobileCLIP S1",
1213
+ description: "MobileCLIP S1 — Apple balanced CLIP vision encoder, 512-dim, 256×256 (90 MB ONNX)",
1214
+ inputSize: {
1215
+ width: 256,
1216
+ height: 256
1217
+ },
1218
+ labels: [{
1219
+ id: "embedding",
1220
+ name: "CLIP Embedding"
1221
+ }],
1222
+ preprocessMode: "resize",
1223
+ inputNormalization: "none",
1224
+ formats: {
1225
+ onnx: {
1226
+ url: hf("clip/mobileclip-s1/onnx/camstack-mobileclip-s1-vision.onnx"),
1227
+ sizeMB: 90
1228
+ },
1229
+ openvino: ovFormat(hf("clip/mobileclip-s1/openvino/camstack-mobileclip-s1-vision.xml"), 55),
1230
+ coreml: {
1231
+ url: hf("clip/mobileclip-s1/coreml/camstack-mobileclip-s1-vision.mlpackage"),
1232
+ sizeMB: 65,
1233
+ isDirectory: true,
1234
+ files: [...MLPACKAGE_FILES],
1235
+ runtimes: ["python"]
1236
+ }
1237
+ }
1238
+ },
1239
+ {
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)",
1243
+ inputSize: {
1244
+ width: 256,
1245
+ height: 256
1246
+ },
1247
+ labels: [{
1248
+ id: "embedding",
1249
+ name: "CLIP Embedding"
1250
+ }],
1251
+ preprocessMode: "resize",
1252
+ inputNormalization: "none",
1253
+ formats: {
1254
+ onnx: {
1255
+ url: hf("clip/mobileclip-s2/onnx/camstack-mobileclip-s2-vision.onnx"),
1256
+ sizeMB: 150
1257
+ },
1258
+ openvino: ovFormat(hf("clip/mobileclip-s2/openvino/camstack-mobileclip-s2-vision.xml"), 90),
1259
+ coreml: {
1260
+ url: hf("clip/mobileclip-s2/coreml/camstack-mobileclip-s2-vision.mlpackage"),
1261
+ sizeMB: 110,
1262
+ isDirectory: true,
1263
+ files: [...MLPACKAGE_FILES],
1264
+ runtimes: ["python"]
1265
+ }
1266
+ }
1267
+ }
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)",
1273
+ inputSize: {
1274
+ width: 1,
1275
+ height: 16e3
1276
+ },
1277
+ labels: [],
1278
+ preprocessMode: "resize",
1279
+ formats: {
1280
+ onnx: {
1281
+ url: hf("audioClassification/yamnet/onnx/camstack-yamnet.onnx"),
1282
+ sizeMB: 3.2
1283
+ },
1284
+ openvino: ovFormat(hf("audioClassification/yamnet/openvino/camstack-yamnet.xml"), 3.2),
1285
+ coreml: {
1286
+ url: hf("audioClassification/yamnet/onnx/camstack-yamnet.onnx"),
1287
+ sizeMB: 3.2,
1288
+ runtimes: ["python"]
1289
+ }
1290
+ }
1291
+ }, {
1292
+ id: "apple-soundanalysis",
1293
+ name: "Apple SoundAnalysis",
1294
+ description: "macOS built-in — 303 sound categories, Neural Engine accelerated, zero download",
1295
+ inputSize: {
1296
+ width: 1,
1297
+ height: 16e3
1298
+ },
1299
+ labels: [],
1300
+ preprocessMode: "resize",
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
+ ];
1410
+ }
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
+ }];
1459
+ }
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
+ }];
1498
+ }
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 ?? "";
1719
+ }
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;
1847
+ }
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
+ };
1854
+ }
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;
1428
1905
  }
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
1445
- },
1446
- coreml: {
1447
- url: hfScrypted("coreml/scrypted_yolov9t_relu/scrypted_yolov9t_relu.mlpackage"),
1448
- sizeMB: 4,
1449
- isDirectory: true,
1450
- files: [...MLPACKAGE_FILES],
1451
- runtimes: ["python"]
1452
- },
1453
- openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9t_relu.xml"), 6)
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}`);
1943
+ }
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}`));
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;
1454
1977
  }
1455
- },
1456
- {
1457
- id: "scrypted-yolov9s-relu",
1458
- legacy: true,
1459
- name: "YOLOv9 Small ReLU",
1460
- description: "YOLOv9s ReLU improved efficiency, int8 ready",
1461
- inputSize: {
1462
- width: 320,
1463
- height: 320
1464
- },
1465
- labels: [],
1466
- preprocessMode: "letterbox",
1467
- formats: {
1468
- onnx: {
1469
- url: hfScrypted("onnx/scrypted_yolov9s_relu/scrypted_yolov9s_relu.onnx"),
1470
- sizeMB: 27.9
1471
- },
1472
- coreml: {
1473
- url: hfScrypted("coreml/scrypted_yolov9s_relu/scrypted_yolov9s_relu.mlpackage"),
1474
- sizeMB: 14,
1475
- isDirectory: true,
1476
- files: [...MLPACKAGE_FILES],
1477
- runtimes: ["python"]
1478
- },
1479
- openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9s_relu.xml"), 16)
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
+ });
2005
+ }
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;
1480
2038
  }
1481
- },
1482
- {
1483
- id: "scrypted-yolov9c-relu",
1484
- legacy: true,
1485
- name: "YOLOv9 C ReLU",
1486
- description: "YOLOv9c ReLU high-accuracy, int8 ready",
1487
- inputSize: {
1488
- width: 320,
1489
- height: 320
1490
- },
1491
- labels: [],
1492
- preprocessMode: "letterbox",
1493
- formats: {
1494
- onnx: {
1495
- url: hfScrypted("onnx/scrypted_yolov9c_relu/scrypted_yolov9c_relu.onnx"),
1496
- sizeMB: 96.9
1497
- },
1498
- coreml: {
1499
- url: hfScrypted("coreml/scrypted_yolov9c_relu/scrypted_yolov9c_relu.mlpackage"),
1500
- sizeMB: 48,
1501
- isDirectory: true,
1502
- files: [...MLPACKAGE_FILES],
1503
- runtimes: ["python"]
1504
- },
1505
- openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9c_relu.xml"), 49)
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;
2043
+ }
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
+ });
2110
+ }
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;
1506
2117
  }
1507
- },
1508
- {
1509
- id: "scrypted-yolov9m-relu",
1510
- legacy: true,
1511
- name: "YOLOv9 M ReLU",
1512
- description: "YOLOv9m ReLU — medium, int8 ready",
1513
- inputSize: {
1514
- width: 320,
1515
- height: 320
1516
- },
1517
- labels: [],
1518
- preprocessMode: "letterbox",
1519
- formats: {
1520
- onnx: {
1521
- url: hfScrypted("onnx/scrypted_yolov9m_relu/scrypted_yolov9m_relu.onnx"),
1522
- sizeMB: 76.6
1523
- },
1524
- coreml: {
1525
- url: hfScrypted("coreml/scrypted_yolov9m_relu/scrypted_yolov9m_relu.mlpackage"),
1526
- sizeMB: 38,
1527
- isDirectory: true,
1528
- files: [...MLPACKAGE_FILES],
1529
- runtimes: ["python"]
1530
- },
1531
- openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9m_relu.xml"), 38)
2118
+ return id;
2119
+ }
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
+ }
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
+ }
1532
2155
  }
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
2156
  }
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)
2157
+ rejectAll(err) {
2158
+ const entries = [...this.pending.values()];
2159
+ this.pending.clear();
2160
+ for (const entry of entries) entry.reject(err);
2161
+ }
2162
+ };
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);
2235
+ }
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);
1602
2239
  }
1603
- }];
1604
- var FACE_EMBEDDING_MODELS = [{
1605
- id: "arcface-r100",
1606
- name: "ArcFace R100",
1607
- description: "ArcFace ResNet-100 — high-accuracy face recognition embeddings (512-d)",
1608
- inputSize: {
1609
- width: 112,
1610
- height: 112
1611
- },
1612
- inputLayout: "nhwc",
1613
- labels: [{
1614
- id: "embedding",
1615
- name: "Face Embedding"
1616
- }],
1617
- preprocessMode: "resize",
1618
- faceAlignment: true,
1619
- formats: {
1620
- onnx: {
1621
- url: hf("faceRecognition/arcface/onnx/camstack-arcface-arcface.onnx"),
1622
- sizeMB: 130
1623
- },
1624
- coreml: {
1625
- url: hf("faceRecognition/arcface/coreml/camstack-arcface-r100.mlpackage"),
1626
- sizeMB: 65,
1627
- isDirectory: true,
1628
- files: [...MLPACKAGE_FILES],
1629
- runtimes: ["python"]
1630
- },
1631
- openvino: ovFormat(hf("faceRecognition/arcface/openvino/camstack-arcface-r100.xml"), 65)
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);
1632
2243
  }
1633
- }, {
1634
- id: "inception-resnet-v1",
1635
- name: "Inception ResNet V1",
1636
- description: "FaceNet-style face recognition embeddings (512-d) — hosted on plugin-models HF repo",
1637
- inputSize: {
1638
- width: 160,
1639
- height: 160
1640
- },
1641
- labels: [{
1642
- id: "embedding",
1643
- name: "Face Embedding"
1644
- }],
1645
- preprocessMode: "resize",
1646
- formats: {
1647
- onnx: {
1648
- url: hfScrypted("onnx/inception_resnet_v1/inception_resnet_v1.onnx"),
1649
- sizeMB: 89.6
1650
- },
1651
- coreml: {
1652
- url: hfScrypted("coreml/inception_resnet_v1/inception_resnet_v1.mlpackage"),
1653
- sizeMB: 45,
1654
- isDirectory: true,
1655
- files: [...MLPACKAGE_FILES],
1656
- runtimes: ["python"]
1657
- },
1658
- openvino: ovFormat(hf("faceRecognition/inception-resnet-v1/openvino/camstack-inception-resnet-v1.xml"), 45)
2244
+ async inferCached(modelIndex, frameId) {
2245
+ const w = this.pickWorker();
2246
+ return this.trackDroppedResponse(await w.inferCached(this.encodeModelByte(modelIndex), frameId), modelIndex);
1659
2247
  }
1660
- }];
1661
- var PLATE_DETECTION_MODELS = [{
1662
- id: "yolov8n-plate",
1663
- name: "YOLOv8 Nano License Plate",
1664
- description: "YOLOv8 Nano fine-tuned for license plate detection",
1665
- inputSize: {
1666
- width: 640,
1667
- height: 640
1668
- },
1669
- labels: [{
1670
- id: "plate",
1671
- name: "License Plate"
1672
- }],
1673
- preprocessMode: "letterbox",
1674
- formats: {
1675
- onnx: {
1676
- url: hf("plateDetection/yolov8-plate/onnx/camstack-yolov8n-plate.onnx"),
1677
- sizeMB: 12
1678
- },
1679
- coreml: {
1680
- url: hf("plateDetection/yolov8-plate/coreml/camstack-yolov8n-plate.mlpackage"),
1681
- sizeMB: 5.9,
1682
- isDirectory: true,
1683
- files: [...MLPACKAGE_FILES],
1684
- runtimes: ["python"]
1685
- },
1686
- openvino: ovFormat(hf("plateDetection/yolov8-plate/openvino/camstack-yolov8n-plate.xml"), 6.1)
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;
1687
2255
  }
1688
- }];
1689
- var PLATE_OCR_MODELS = [{
1690
- id: "vgg-english-g2",
1691
- name: "VGG English G2",
1692
- description: "EasyOCR VGG English G2 — text recognition for license plates and scene text",
1693
- inputSize: {
1694
- width: 384,
1695
- height: 64
1696
- },
1697
- labels: [{
1698
- id: "text",
1699
- name: "Plate Text"
1700
- }],
1701
- preprocessMode: "resize",
1702
- formats: {
1703
- onnx: {
1704
- url: hfScrypted("onnx/vgg_english_g2/vgg_english_g2.onnx"),
1705
- sizeMB: 14.4
1706
- },
1707
- coreml: {
1708
- url: hfScrypted("coreml/vgg_english_g2/vgg_english_g2.mlpackage"),
1709
- sizeMB: 11.2,
1710
- isDirectory: true,
1711
- files: [...MLPACKAGE_FILES],
1712
- runtimes: ["python"]
1713
- },
1714
- openvino: ovFormat(hf("plateRecognition/vgg_english_g2/openvino/vgg_english_g2.xml"), 7.2)
2256
+ getHandle(modelIndex) {
2257
+ return new PoolHandle(this, modelIndex);
1715
2258
  }
1716
- }];
1717
- var ANIMAL_CLASSIFIER_MODELS = [{
1718
- id: "animals-10",
1719
- name: "Animal Classifier (10)",
1720
- description: "ViT-based animal type classifier — cat, cow, dog, dolphin, eagle, panda, horse, monkey, sheep, spider",
1721
- inputSize: {
1722
- width: 224,
1723
- height: 224
1724
- },
1725
- inputNormalization: "imagenet",
1726
- labels: [{
1727
- id: "animal-type",
1728
- name: "Animal Type"
1729
- }],
1730
- preprocessMode: "resize",
1731
- formats: {
1732
- onnx: {
1733
- url: hf("animalClassification/animals-10/onnx/camstack-animals-10.onnx"),
1734
- sizeMB: 328
1735
- },
1736
- coreml: {
1737
- url: hf("animalClassification/animals-10/coreml/camstack-animals-10.mlpackage"),
1738
- sizeMB: 164,
1739
- isDirectory: true,
1740
- files: [...MLPACKAGE_FILES],
1741
- runtimes: ["python"]
1742
- },
1743
- openvino: ovFormat(hf("animalClassification/animals-10/openvino/camstack-animals-10.xml"), 164)
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];
1744
2336
  }
1745
- }];
1746
- var BIRD_CLASSIFIER_MODELS = [{
1747
- id: "bird-nabirds-404",
1748
- name: "NABirds (404 species)",
1749
- description: "ResNet50 trained on NABirds — 404 North American species",
1750
- inputSize: {
1751
- width: 224,
1752
- height: 224
1753
- },
1754
- inputNormalization: "imagenet",
1755
- labels: [{
1756
- id: "species",
1757
- name: "Bird Species"
1758
- }],
1759
- preprocessMode: "resize",
1760
- formats: {
1761
- onnx: {
1762
- url: hf("animalClassification/bird-nabirds/onnx/camstack-bird-nabirds-404.onnx"),
1763
- sizeMB: 93
1764
- },
1765
- coreml: {
1766
- url: hf("animalClassification/bird-nabirds/coreml/camstack-bird-nabirds-404.mlpackage"),
1767
- sizeMB: 47,
1768
- isDirectory: true,
1769
- files: [...MLPACKAGE_FILES],
1770
- runtimes: ["python"]
1771
- },
1772
- openvino: ovFormat(hf("animalClassification/bird-nabirds/openvino/camstack-bird-nabirds-404.xml"), 47)
1773
- },
1774
- extraFiles: [{
1775
- url: hf("animalClassification/bird-nabirds/onnx/camstack-bird-nabirds-404-labels.json"),
1776
- filename: "camstack-bird-nabirds-404-labels.json",
1777
- sizeMB: .02
1778
- }]
1779
- }];
1780
- var VEHICLE_CLASSIFIER_MODELS = [{
1781
- id: "vehicle-type-efficientnet",
1782
- name: "Vehicle Type (EfficientNet)",
1783
- description: "EfficientNet-B4 vehicle make/model/year classifier — 8,949 classes from VMMRdb",
1784
- inputSize: {
1785
- width: 380,
1786
- height: 380
1787
- },
1788
- inputNormalization: "imagenet",
1789
- labels: [{
1790
- id: "vehicle-type",
1791
- name: "Vehicle Type"
1792
- }],
1793
- preprocessMode: "resize",
1794
- formats: {
1795
- onnx: {
1796
- url: hf("vehicleClassification/efficientnet/onnx/camstack-vehicle-type-efficientnet.onnx"),
1797
- sizeMB: 135
1798
- },
1799
- coreml: {
1800
- url: hf("vehicleClassification/efficientnet/coreml/camstack-vehicle-type-efficientnet.mlpackage"),
1801
- sizeMB: 10,
1802
- isDirectory: true,
1803
- files: [...MLPACKAGE_FILES],
1804
- runtimes: ["python"]
1805
- },
1806
- openvino: ovFormat(hf("vehicleClassification/efficientnet/openvino/camstack-vehicle-type-efficientnet.xml"), 68)
1807
- },
1808
- extraFiles: [{
1809
- url: hf("vehicleClassification/efficientnet/camstack-vehicle-type-labels.json"),
1810
- filename: "camstack-vehicle-type-labels.json",
1811
- sizeMB: .2
1812
- }]
1813
- }];
1814
- var SEGMENTATION_REFINER_MODELS = [{
1815
- id: "u2netp",
1816
- name: "U2-Net Portable",
1817
- description: "U2-Net-P — ultra-lightweight salient object segmentation (4.7 MB)",
1818
- inputSize: {
1819
- width: 320,
1820
- height: 320
1821
- },
1822
- labels: [{
1823
- id: "mask",
1824
- name: "Segmentation Mask"
1825
- }],
1826
- preprocessMode: "resize",
1827
- formats: {
1828
- onnx: {
1829
- url: hf("segmentationRefiner/u2netp/onnx/camstack-u2netp.onnx"),
1830
- sizeMB: 5
1831
- },
1832
- coreml: {
1833
- url: hf("segmentationRefiner/u2netp/coreml/camstack-u2netp.mlpackage"),
1834
- sizeMB: 3,
1835
- isDirectory: true,
1836
- files: [...MLPACKAGE_FILES],
1837
- runtimes: ["python"]
1838
- },
1839
- openvino: ovFormat(hf("segmentationRefiner/u2netp/openvino/camstack-u2netp.xml"), 2.5)
2337
+ encodeModelByte(modelIndex) {
2338
+ if (modelIndex < 0 || modelIndex > 254) throw new Error(`Model index ${modelIndex} out of range (0-254)`);
2339
+ return modelIndex;
1840
2340
  }
1841
- }];
1842
- var INSTANCE_SEGMENTATION_MODELS = [
1843
- {
1844
- id: "yolo26n-seg",
1845
- name: "YOLO26 Nano Seg",
1846
- description: "YOLO26 Nano Segmentation — ultra-lightweight instance segmentation with masks",
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)
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;
2371
+ }
2372
+ //#endregion
2373
+ //#region src/detection-pipeline/engine/step-utils.ts
2374
+ /**
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.
2378
+ */
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);
1866
2387
  }
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)
2388
+ };
2389
+ walk(steps);
2390
+ return result;
2391
+ }
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);
1891
2448
  }
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)
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
+ });
1916
2508
  }
2509
+ return out;
1917
2510
  }
1918
- ];
1919
- var CLIP_EMBEDDING_MODELS = [
1920
- {
1921
- id: "mobileclip-s0",
1922
- name: "MobileCLIP S0",
1923
- description: "MobileCLIP S0 Apple ultra-fast CLIP vision encoder, 512-dim, 256×256 (40 MB ONNX)",
1924
- inputSize: {
1925
- width: 256,
1926
- height: 256
1927
- },
1928
- labels: [{
1929
- id: "embedding",
1930
- name: "CLIP Embedding"
1931
- }],
1932
- preprocessMode: "resize",
1933
- inputNormalization: "none",
1934
- formats: {
1935
- onnx: {
1936
- url: hf("clip/mobileclip-s0/onnx/camstack-mobileclip-s0-vision.onnx"),
1937
- sizeMB: 40
1938
- },
1939
- openvino: ovFormat(hf("clip/mobileclip-s0/openvino/camstack-mobileclip-s0-vision.xml"), 25),
1940
- coreml: {
1941
- url: hf("clip/mobileclip-s0/coreml/camstack-mobileclip-s0-vision.mlpackage"),
1942
- sizeMB: 30,
1943
- isDirectory: true,
1944
- files: [...MLPACKAGE_FILES],
1945
- runtimes: ["python"]
1946
- }
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);
1947
2524
  }
1948
- },
1949
- {
1950
- id: "mobileclip-s1",
1951
- name: "MobileCLIP S1",
1952
- description: "MobileCLIP S1 Apple balanced CLIP vision encoder, 512-dim, 256×256 (90 MB ONNX)",
1953
- inputSize: {
1954
- width: 256,
1955
- height: 256
1956
- },
1957
- labels: [{
1958
- id: "embedding",
1959
- name: "CLIP Embedding"
1960
- }],
1961
- preprocessMode: "resize",
1962
- inputNormalization: "none",
1963
- formats: {
1964
- onnx: {
1965
- url: hf("clip/mobileclip-s1/onnx/camstack-mobileclip-s1-vision.onnx"),
1966
- sizeMB: 90
1967
- },
1968
- openvino: ovFormat(hf("clip/mobileclip-s1/openvino/camstack-mobileclip-s1-vision.xml"), 55),
1969
- coreml: {
1970
- url: hf("clip/mobileclip-s1/coreml/camstack-mobileclip-s1-vision.mlpackage"),
1971
- sizeMB: 65,
1972
- isDirectory: true,
1973
- files: [...MLPACKAGE_FILES],
1974
- runtimes: ["python"]
1975
- }
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);
1976
2536
  }
1977
- },
1978
- {
1979
- id: "mobileclip-s2",
1980
- name: "MobileCLIP S2",
1981
- description: "MobileCLIP S2 — Apple high-accuracy CLIP vision encoder, 512-dim, 256×256 (150 MB ONNX)",
1982
- inputSize: {
1983
- width: 256,
1984
- height: 256
1985
- },
1986
- labels: [{
1987
- id: "embedding",
1988
- name: "CLIP Embedding"
1989
- }],
1990
- preprocessMode: "resize",
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"]
2004
- }
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
+ } });
2005
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;
2006
2575
  }
2007
- ];
2008
- var AUDIO_CLASSIFIER_MODELS = [{
2009
- id: "yamnet-onnx",
2010
- name: "YAMNet",
2011
- description: "Google YAMNet — 521-class audio event classifier (3.2 MB ONNX, runs on any platform)",
2012
- inputSize: {
2013
- width: 1,
2014
- height: 16e3
2015
- },
2016
- labels: [],
2017
- preprocessMode: "resize",
2018
- formats: {
2019
- onnx: {
2020
- url: hf("audioClassification/yamnet/onnx/camstack-yamnet.onnx"),
2021
- sizeMB: 3.2
2022
- },
2023
- openvino: ovFormat(hf("audioClassification/yamnet/openvino/camstack-yamnet.xml"), 3.2),
2024
- coreml: {
2025
- url: hf("audioClassification/yamnet/onnx/camstack-yamnet.onnx"),
2026
- sizeMB: 3.2,
2027
- runtimes: ["python"]
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);
2028
2583
  }
2029
2584
  }
2030
- }, {
2031
- id: "apple-soundanalysis",
2032
- name: "Apple SoundAnalysis",
2033
- description: "macOS built-in — 303 sound categories, Neural Engine accelerated, zero download",
2034
- inputSize: {
2035
- width: 1,
2036
- height: 16e3
2037
- },
2038
- labels: [],
2039
- preprocessMode: "resize",
2040
- formats: { coreml: {
2041
- url: "",
2042
- sizeMB: 0
2043
- } }
2044
- }];
2045
- //#endregion
2046
- //#region src/detection-pipeline/registry/step-definitions.ts
2047
- var ANIMAL_10_LABELS = [
2048
- "cat",
2049
- "cow",
2050
- "dog",
2051
- "dolphin",
2052
- "eagle",
2053
- "giant panda",
2054
- "horse",
2055
- "monkey",
2056
- "sheep",
2057
- "spider"
2058
- ];
2059
- var VGG_G2_CHARSET = ["", ..."0123456789!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split("")];
2060
- var PipelineStepBase = class {
2061
- definition;
2062
- constructor(definition) {
2063
- this.definition = definition;
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;
2064
2596
  }
2065
- getConfigSchema() {
2066
- return [];
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
+ }
2067
2606
  }
2068
- };
2069
- var ObjectDetectionStep = class {
2070
- definition = {
2071
- id: "object-detection",
2072
- name: "Object Detection",
2073
- slot: "detector",
2074
- postprocessor: "yolo",
2075
- extractMode: "full-frame",
2076
- inputClasses: null,
2077
- outputClasses: [
2078
- "person",
2079
- "vehicle",
2080
- "animal"
2081
- ],
2082
- models: [...OBJECT_DETECTION_MODELS],
2083
- defaultModelId: "yolo26n",
2084
- defaultConfidence: .5,
2085
- labels: require_dist.COCO_80_LABELS.map((l) => l.id),
2086
- classMap: require_dist.COCO_TO_MACRO
2087
- };
2088
- getConfigSchema() {
2089
- return [
2090
- {
2091
- type: "multiselect",
2092
- key: "enabledMacroClasses",
2093
- label: "Enabled Classes",
2094
- description: "Which macro classes to detect (empty = all three).",
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
2147
- }
2148
- ];
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;
2149
2613
  }
2150
- };
2151
- var AudioClassifierStep = class {
2152
- definition = {
2153
- id: "audio-classifier",
2154
- name: "Audio Classifier",
2155
- slot: "audio-classifier",
2156
- postprocessor: "yamnet",
2157
- extractMode: "full-frame",
2158
- inputClasses: null,
2159
- outputClasses: ["audio"],
2160
- models: [...AUDIO_CLASSIFIER_MODELS],
2161
- defaultModelId: "yamnet-onnx",
2162
- defaultModelIdByFormat: { coreml: "apple-soundanalysis" },
2163
- enabledByDefault: true,
2164
- defaultConfidence: .3
2165
- };
2166
- getConfigSchema() {
2167
- return [{
2168
- type: "multiselect",
2169
- key: "enabledAudioClasses",
2170
- label: "Audio Classes",
2171
- description: "Which audio macro classes to surface (empty = all).",
2172
- options: require_dist.AUDIO_MACRO_LABELS.map((l) => ({
2173
- value: l.id,
2174
- label: `${l.icon ?? ""} ${l.name}`.trim()
2175
- })),
2176
- default: [
2177
- "scream",
2178
- "crying",
2179
- "dog",
2180
- "alarm",
2181
- "doorbell",
2182
- "glass_breaking",
2183
- "gunshot",
2184
- "siren",
2185
- "footsteps"
2186
- ]
2187
- }, {
2188
- type: "slider",
2189
- key: "minConfidence",
2190
- label: "Min Confidence",
2191
- description: "Drop predictions whose score is below this threshold.",
2192
- default: .3,
2193
- min: 0,
2194
- max: 1,
2195
- step: .05,
2196
- showValue: true
2197
- }];
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;
2198
2618
  }
2199
- };
2200
- var AnimalClassifierStep = class {
2201
- definition = {
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
- }];
2619
+ touch(entry) {
2620
+ entry.lruTick = ++this.lruClock;
2237
2621
  }
2238
- };
2239
- var ClassifierWithMinConfidence = class extends PipelineStepBase {
2240
- getConfigSchema() {
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
- }];
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
+ };
2252
2650
  }
2253
2651
  };
2254
- var STEP_FACE_DETECTION = new PipelineStepBase({
2255
- id: "face-detection",
2256
- name: "Face Detection",
2257
- slot: "cropper",
2258
- postprocessor: "scrfd",
2259
- extractMode: "crop-roi",
2260
- inputClasses: ["person"],
2261
- outputClasses: ["face"],
2262
- models: [...FACE_DETECTION_MODELS],
2263
- defaultModelId: "scrfd-2.5g",
2264
- defaultConfidence: .5,
2265
- defaultMinParentScore: .7
2266
- });
2267
- var STEP_FACE_EMBEDDING = new PipelineStepBase({
2268
- id: "face-embedding",
2269
- name: "Face Embedding",
2270
- slot: "classifier",
2271
- postprocessor: "arcface",
2272
- extractMode: "crop-roi",
2273
- inputClasses: ["face"],
2274
- outputClasses: ["identity"],
2275
- models: [...FACE_EMBEDDING_MODELS],
2276
- defaultModelId: "arcface-r100",
2277
- defaultConfidence: 0
2278
- });
2279
- var STEP_CLIP_EMBEDDING = new PipelineStepBase({
2280
- id: "clip-embedding",
2281
- name: "CLIP Embedding",
2282
- slot: "classifier",
2283
- postprocessor: "clip",
2284
- extractMode: "crop-roi",
2285
- inputClasses: null,
2286
- outputClasses: ["embedding"],
2287
- models: [...CLIP_EMBEDDING_MODELS],
2288
- defaultModelId: "mobileclip-s1",
2289
- enabledByDefault: false,
2290
- defaultConfidence: 0,
2291
- defaultMinParentScore: .4,
2292
- group: "CLIP"
2293
- });
2294
- var STEP_PLATE_DETECTION = new PipelineStepBase({
2295
- id: "plate-detection",
2296
- name: "Plate Detection",
2297
- slot: "cropper",
2298
- postprocessor: "yolo",
2299
- extractMode: "crop-roi",
2300
- inputClasses: ["vehicle"],
2301
- outputClasses: ["plate"],
2302
- models: [...PLATE_DETECTION_MODELS],
2303
- defaultModelId: "yolov8n-plate",
2304
- defaultConfidence: .5,
2305
- defaultMinParentScore: .7,
2306
- labels: ["plate"]
2307
- });
2308
- var STEP_PLATE_OCR = new PipelineStepBase({
2309
- id: "plate-ocr",
2310
- name: "Plate OCR",
2311
- slot: "classifier",
2312
- postprocessor: "ctc",
2313
- extractMode: "crop-roi",
2314
- inputClasses: ["plate"],
2315
- outputClasses: ["plate-text"],
2316
- models: [...PLATE_OCR_MODELS],
2317
- defaultModelId: "vgg-english-g2",
2318
- defaultConfidence: 0,
2319
- charset: [...VGG_G2_CHARSET]
2320
- });
2321
- var STEP_BIRD_CLASSIFIER = new ClassifierWithMinConfidence({
2322
- id: "bird-classifier",
2323
- name: "Bird Classifier",
2324
- slot: "classifier",
2325
- postprocessor: "softmax",
2326
- extractMode: "crop-roi",
2327
- inputClasses: ["animal"],
2328
- outputClasses: ["species"],
2329
- models: [...BIRD_CLASSIFIER_MODELS],
2330
- defaultModelId: "bird-nabirds-404",
2331
- enabledByDefault: false,
2332
- defaultConfidence: .2
2333
- });
2334
- var STEP_VEHICLE_CLASSIFIER = new ClassifierWithMinConfidence({
2335
- id: "vehicle-classifier",
2336
- name: "Vehicle Classifier",
2337
- slot: "classifier",
2338
- postprocessor: "softmax",
2339
- extractMode: "crop-roi",
2340
- inputClasses: ["vehicle"],
2341
- outputClasses: ["vehicle-type"],
2342
- models: [...VEHICLE_CLASSIFIER_MODELS],
2343
- defaultModelId: "vehicle-type-efficientnet",
2344
- enabledByDefault: false,
2345
- defaultConfidence: .3
2346
- });
2347
- var STEP_SEGMENTATION = new PipelineStepBase({
2348
- id: "segmentation",
2349
- name: "Segmentation",
2350
- slot: "refiner",
2351
- postprocessor: "saliency",
2352
- extractMode: "crop-roi",
2353
- inputClasses: null,
2354
- outputClasses: ["mask"],
2355
- models: [...SEGMENTATION_REFINER_MODELS],
2356
- defaultModelId: "u2netp",
2357
- enabledByDefault: false,
2358
- defaultConfidence: 0,
2359
- group: "Segmentation"
2360
- });
2361
- new PipelineStepBase({
2362
- id: "instance-segmentation",
2363
- name: "Instance Segmentation",
2364
- slot: "refiner",
2365
- postprocessor: "yolo-seg",
2366
- extractMode: "full-frame",
2367
- inputClasses: null,
2368
- outputClasses: ["mask"],
2369
- models: [...INSTANCE_SEGMENTATION_MODELS],
2370
- defaultModelId: "yolo26n-seg",
2371
- enabledByDefault: false,
2372
- defaultConfidence: .25,
2373
- labels: require_dist.COCO_80_LABELS.map((l) => l.id),
2374
- classMap: require_dist.COCO_TO_MACRO,
2375
- group: "Segmentation"
2376
- });
2377
- var STEP_OBJECT_DETECTION_INSTANCE = new ObjectDetectionStep();
2378
- var STEP_AUDIO_CLASSIFIER_INSTANCE = new AudioClassifierStep();
2379
- var ALL_PIPELINE_STEPS = [
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;
2410
- }
2411
- /**
2412
- * Get the default model ID for a step given the current model format.
2413
- *
2414
- * Resolution order:
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.
2425
- */
2426
- function getDefaultModelForFormat(stepId, format) {
2427
- const def = getStepDefinition(stepId);
2428
- const hasFormatBuild = (modelId) => def.models.find((m) => m.id === modelId)?.formats[format] !== void 0;
2429
- const declaredForFormat = def.defaultModelIdByFormat?.[format];
2430
- if (declaredForFormat !== void 0 && hasFormatBuild(declaredForFormat)) return declaredForFormat;
2431
- if (hasFormatBuild(def.defaultModelId)) return def.defaultModelId;
2432
- const available = def.models.filter((m) => m.formats[format] && m.legacy !== true);
2433
- if (available.length === 0) return def.defaultModelId;
2434
- return [...available].toSorted((a, b) => {
2435
- return (a.formats[format]?.sizeMB ?? Infinity) - (b.formats[format]?.sizeMB ?? Infinity);
2436
- })[0].id;
2437
- }
2438
- /**
2439
- * The single node-local authority for turning a CHOSEN model (or none) into a
2440
- * concrete model that has a build for this node's engine `format`. Fuses the
2441
- * two previously-duplicated decisions (orchestrator `pickDefaultModelId` +
2442
- * provider `substituteIncompatibleModels`):
2443
- *
2444
- * - chosen === null → the addon's default model for `format`.
2445
- * - chosen is a catalog model → keep it if it has a `format` build; else
2446
- * the addon's default model for `format`
2447
- * (a substitution — the caller surfaces it).
2448
- * - chosen is NOT in the catalog (custom/unknown) → returned unchanged; the
2449
- * operator owns a custom model's format compat.
2450
- * - unknown stepId → chosen returned unchanged (never throws).
2451
- */
2452
- function resolveModelForFormat(stepId, chosenModelId, format) {
2453
- let def;
2454
- try {
2455
- def = getStepDefinition(stepId);
2456
- } catch {
2457
- return chosenModelId ?? "";
2458
- }
2459
- if (chosenModelId === null) return getDefaultModelForFormat(stepId, format);
2460
- const entry = def.models.find((m) => m.id === chosenModelId);
2461
- if (!entry) return chosenModelId;
2462
- if (entry.formats[format]) return chosenModelId;
2463
- return getDefaultModelForFormat(stepId, format);
2464
- }
2465
2652
  //#endregion
2466
2653
  //#region src/detection-pipeline/engine/engine-factory.ts
2467
2654
  var BACKEND_TO_POOL_RUNTIME = {
@@ -4794,6 +4981,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4794
4981
  writeStore;
4795
4982
  /** Read per-device settings. */
4796
4983
  readDeviceStore;
4984
+ /** Delete all per-device settings for a device (used by `resetToDefault`). */
4985
+ clearDeviceStore;
4797
4986
  constructor(settings, modelsDir, logger, eventBus = null, detectionConfigSchemaSource = null, executorOptions = {}) {
4798
4987
  this.modelsDir = modelsDir;
4799
4988
  this.eventBus = eventBus;
@@ -4803,6 +4992,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4803
4992
  this.readStore = () => settings.readAddonStore();
4804
4993
  this.writeStore = (patch) => settings.writeAddonStore(patch);
4805
4994
  this.readDeviceStore = settings.readDeviceStore ?? (async () => ({}));
4995
+ const clearDeviceStore = settings.clearDeviceStore;
4996
+ this.clearDeviceStore = clearDeviceStore ? (deviceId) => clearDeviceStore.call(settings, deviceId) : async () => void 0;
4806
4997
  this.currentEngine = ONNX_FLOOR;
4807
4998
  this.log.info("Engine pick pending (placeholder until probe / persisted selection)", { meta: {
4808
4999
  runtime: this.currentEngine.runtime,
@@ -5269,7 +5460,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5269
5460
  };
5270
5461
  }
5271
5462
  async getDefaultSteps(engine) {
5272
- return buildDefaultStepTree(engine.format);
5463
+ const detectionModelId = resolveDefaultDetectionModel(engine.backend, engine.device ?? null, engine.format);
5464
+ return buildDefaultStepTree(engine.format, detectionModelId);
5273
5465
  }
5274
5466
  /**
5275
5467
  * Resolve every step's model for this node's engine format via the shared
@@ -5316,7 +5508,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5316
5508
  if (this.currentSteps) return this.currentSteps;
5317
5509
  const raw = (await this.readStore())[KEY_STEPS];
5318
5510
  if (!raw) {
5319
- const defaults = buildDefaultStepTree(this.currentEngine.format);
5511
+ const detectionModelId = resolveDefaultDetectionModel(this.currentEngine.backend, this.currentEngine.device ?? null, this.currentEngine.format);
5512
+ const defaults = buildDefaultStepTree(this.currentEngine.format, detectionModelId);
5320
5513
  if (defaults.length === 0) return null;
5321
5514
  this.currentSteps = this.resolveStepModels(defaults);
5322
5515
  this.writeStore({ [KEY_STEPS]: JSON.stringify(defaults) });
@@ -6789,6 +6982,82 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6789
6982
  this.log.info("videoPipelineSteps updated", { meta: { addons: Object.keys(sanitized).length } });
6790
6983
  return { success: true };
6791
6984
  }
6985
+ /**
6986
+ * Reset THIS node's detection-pipeline to its factory defaults, forcing
6987
+ * regeneration of the hardware-aware default object-detection model.
6988
+ *
6989
+ * Node-routed (the cap's default `nodeIdMode: 'routing'`): the generated
6990
+ * cap-router strips `nodeId` and dispatches to the target node, so this
6991
+ * method runs ON the node being reset and takes no argument.
6992
+ *
6993
+ * Clears three things:
6994
+ * 1. the persisted GLOBAL pipeline seed (`KEY_STEPS`) — nulled in the
6995
+ * store AND the in-memory `currentSteps` cache, so the subsequent
6996
+ * `getGlobalSteps()` re-seeds the hardware-aware default tree fresh
6997
+ * (`resolveDefaultDetectionModel` against this node's live engine)
6998
+ * rather than returning the stale cached blob.
6999
+ * 2. the LEGACY engine blob (`KEY_ENGINE`). The per-node engine cascade
7000
+ * (`engineBackend@<node>` / `engineDevice@<node>`) is intentionally
7001
+ * left intact — the operator's engine selection is preserved and the
7002
+ * default is regenerated against it.
7003
+ * 3. every device's PER-CAMERA step overrides (the per-device store the
7004
+ * object-detection root reads via `applyDeviceOverridesToTree`).
7005
+ *
7006
+ * Idempotent: a second call re-seeds the identical default and clears
7007
+ * already-empty per-device stores harmlessly. Returns the regenerated
7008
+ * object-detection model id (null when the default tree has no detector
7009
+ * for this node's format).
7010
+ */
7011
+ async resetToDefault() {
7012
+ await this.writeStore({
7013
+ [KEY_STEPS]: null,
7014
+ [KEY_ENGINE]: null
7015
+ });
7016
+ this.currentSteps = null;
7017
+ const clearedDevices = await this.clearAllDeviceOverrides();
7018
+ const regeneratedModelId = ((await this.getGlobalSteps())?.find((s) => s.slot === "detector") ?? null)?.modelId ?? null;
7019
+ this.log.info("pipeline reset to default", { meta: {
7020
+ clearedGlobalSteps: true,
7021
+ clearedEngineBlob: true,
7022
+ clearedDeviceOverrides: clearedDevices,
7023
+ regeneratedModelId
7024
+ } });
7025
+ return {
7026
+ success: true,
7027
+ regeneratedModelId
7028
+ };
7029
+ }
7030
+ /**
7031
+ * Clear the per-device pipeline-step overrides for every known device.
7032
+ * Returns the count of devices whose store was cleared. There is no
7033
+ * bulk-list primitive over the per-device store, so devices are
7034
+ * enumerated via the device-manager and cleared one by one
7035
+ * (`clearDeviceStore` is idempotent — a device with no overrides is a
7036
+ * no-op). Never throws: a device-manager outage degrades to "cleared
7037
+ * nothing" so the global reset still completes.
7038
+ */
7039
+ async clearAllDeviceOverrides() {
7040
+ const api = this.addonCtx?.api;
7041
+ if (!api) return 0;
7042
+ let devices = [];
7043
+ try {
7044
+ devices = await api.deviceManager.listAll.query({});
7045
+ } catch (err) {
7046
+ this.log.warn("resetToDefault: device enumeration failed — skipping per-device clear", { meta: { error: require_dist.errMsg(err) } });
7047
+ return 0;
7048
+ }
7049
+ let cleared = 0;
7050
+ for (const device of devices) try {
7051
+ await this.clearDeviceStore(device.id);
7052
+ cleared++;
7053
+ } catch (err) {
7054
+ this.log.warn("resetToDefault: failed to clear per-device overrides for device", { meta: {
7055
+ deviceId: device.id,
7056
+ error: require_dist.errMsg(err)
7057
+ } });
7058
+ }
7059
+ return cleared;
7060
+ }
6792
7061
  async listLoadedEngines() {
6793
7062
  const out = [];
6794
7063
  const overrideFactory = this.overrideCache?.factory ?? null;
@@ -7114,7 +7383,7 @@ function resolveChildSlots(slot) {
7114
7383
  * - Covers 90% of NVR use cases (person/vehicle/animal detection + face detection)
7115
7384
  * - Users opt-in to heavy classifiers (vehicle type, plate OCR, bird species) when needed
7116
7385
  */
7117
- function buildDefaultStepTree(format) {
7386
+ function buildDefaultStepTree(format, objectDetectionModelId) {
7118
7387
  const makeStep = (stepId, children, opts) => {
7119
7388
  const def = getStepDefinition(stepId);
7120
7389
  if (!def.models.some((m) => m.formats[format])) return null;
@@ -7140,7 +7409,7 @@ function buildDefaultStepTree(format) {
7140
7409
  makeStep("bird-classifier", [], { enabled: false }),
7141
7410
  makeStep("vehicle-classifier", [], { enabled: false }),
7142
7411
  makeStep("segmentation", [], { enabled: false })
7143
- ].filter((s) => s !== null));
7412
+ ].filter((s) => s !== null), objectDetectionModelId !== void 0 ? { modelId: objectDetectionModelId } : void 0);
7144
7413
  const audioStep = makeStep("audio-classifier", []);
7145
7414
  const steps = [];
7146
7415
  if (root) steps.push(root);