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