@camstack/addon-pipeline 1.1.44 → 1.1.46

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