@camstack/addon-pipeline 1.1.65 → 1.1.67

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 (40) 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 +83 -22
  4. package/dist/detection-pipeline/index.mjs +83 -22
  5. package/dist/{dist-WW3gH8hF.js → dist-BxxVKcnw.js} +56 -5
  6. package/dist/{dist-D6tHM9GA.mjs → dist-DkGHtAaK.mjs} +56 -5
  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 +88 -22
  10. package/dist/pipeline-runner/index.mjs +88 -22
  11. package/dist/recorder/index.js +141 -1
  12. package/dist/recorder/index.mjs +141 -1
  13. package/dist/session-decode/decode-worker-child.js +185 -14
  14. package/dist/session-decode/decode-worker-child.mjs +185 -14
  15. package/dist/{step-definitions-CXZpwwT0.js → step-definitions-CAs0RD2N.js} +127 -8
  16. package/dist/{step-definitions-Dbsm64_I.mjs → step-definitions-Delhq28w.mjs} +127 -8
  17. package/dist/stream-broker/_stub.js +2 -2
  18. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CbU_HuOC.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CfFsU700.mjs} +2 -2
  19. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D68Lj69v.mjs +26 -0
  20. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DlALQ_wp.mjs +26 -0
  21. package/dist/stream-broker/{hostInit-QaDEOY0Z.mjs → hostInit-D-S5fCDx.mjs} +2 -2
  22. package/dist/stream-broker/index.js +1 -1
  23. package/dist/stream-broker/index.mjs +1 -1
  24. package/dist/stream-broker/remoteEntry.js +1 -1
  25. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-C7H0kVAH.js → MaskShapeCanvas-DI4BY7W2-CRvzj7Of.js} +1 -1
  26. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-CHfEFlEF.js → MotionZonesSettings-NcxxQN8r-CBgvPUB-.js} +1 -1
  27. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-Bc-trVxX.js → PrivacyMaskSettings-APgPLF7p-DH07fe1k.js} +1 -1
  28. package/embed-dist/assets/index-BkoPcGtQ.js +114 -0
  29. package/embed-dist/assets/index-D2pF9Z3W.css +2 -0
  30. package/embed-dist/index.html +2 -2
  31. package/package.json +1 -1
  32. package/python/inference_pool.py +392 -45
  33. package/python/postprocessors/softmax.py +17 -5
  34. package/python/postprocessors/test_softmax.py +52 -0
  35. package/python/test_inference_pool_ov_ppp.py +117 -0
  36. package/python/test_inference_pool_preprocess.py +93 -1
  37. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CxZvj8X1.mjs +0 -26
  38. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BzNJyeNE.mjs +0 -26
  39. package/embed-dist/assets/index-Bm9LaQPH.css +0 -2
  40. package/embed-dist/assets/index-eZGm_itM.js +0 -81
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-D6vf50IK.js");
6
- const require_dist = require("../dist-WW3gH8hF.js");
6
+ const require_dist = require("../dist-BxxVKcnw.js");
7
7
  const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
8
8
  const require_model_download_service_Cp9f4dk6 = require("../model-download-service-Cp9f4dk6-Dv-oFwjN.js");
9
9
  let node_fs = require("node:fs");
@@ -1,5 +1,5 @@
1
1
  import { t as __require } from "../chunk-BdkLduGY.mjs";
2
- import { B as errMsg, K as hydrateSchema, T as mapAudioLabelToMacro, V as BaseAddon, _ as audioAnalyzerCapability, g as audioAnalysisCapability, n as AUDIO_BACKEND_CHOICES, o as DEFAULT_AUDIO_ANALYZER_CONFIG, u as HF_BASE_URL } from "../dist-D6tHM9GA.mjs";
2
+ import { B as errMsg, K as hydrateSchema, T as mapAudioLabelToMacro, V as BaseAddon, _ as audioAnalyzerCapability, g as audioAnalysisCapability, n as AUDIO_BACKEND_CHOICES, o as DEFAULT_AUDIO_ANALYZER_CONFIG, u as HF_BASE_URL } from "../dist-DkGHtAaK.mjs";
3
3
  import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
4
4
  import { i as downloadFile } from "../model-download-service-Cp9f4dk6-CwUb5v3Y.mjs";
5
5
  import * as fs from "node:fs";
@@ -3,8 +3,8 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-D6vf50IK.js");
6
- const require_dist = require("../dist-WW3gH8hF.js");
7
- const require_step_definitions = require("../step-definitions-CXZpwwT0.js");
6
+ const require_dist = require("../dist-BxxVKcnw.js");
7
+ const require_step_definitions = require("../step-definitions-CAs0RD2N.js");
8
8
  const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
9
9
  const require_model_download_service_Cp9f4dk6 = require("../model-download-service-Cp9f4dk6-Dv-oFwjN.js");
10
10
  let node_child_process = require("node:child_process");
@@ -426,6 +426,15 @@ var RAW_FMT_CODE = {
426
426
  bgr: 1,
427
427
  gray: 2
428
428
  };
429
+ /**
430
+ * Per-inference-request reply timeout (ms). Turns a wedged request (worker
431
+ * alive but no reply) into a rejection so every caller settles — runtime frame
432
+ * dispatch drops the frame; the benchmark full-tree run rejects the wedged
433
+ * child (its enclosing try/catch keeps the root detections) instead of hanging
434
+ * the UI on "loading" forever. Generous so legitimate slow inference (cold JIT,
435
+ * large frame) never trips; override via env for constrained hardware.
436
+ */
437
+ var POOL_INFER_TIMEOUT_MS = Math.max(1e3, Number(process.env["CAMSTACK_POOL_INFER_TIMEOUT_MS"]) || 6e4);
429
438
  var PoolHandle = class {
430
439
  pool;
431
440
  modelIndex;
@@ -608,15 +617,16 @@ var PoolWorker = class {
608
617
  rawHeader[9] = fmtCode;
609
618
  return this.dispatchMulti(MSG_INFER_RAW, rawHeader.length + raw.length, [rawHeader, raw]);
610
619
  }
611
- async inferBatch(modelByte, items) {
620
+ async inferBatch(modelByte, items, frameId = 0) {
612
621
  this.ensureReady();
613
622
  if (items.length === 0) return [];
614
- const headerSize = 2 + items.length * 13;
623
+ const headerSize = 6 + items.length * 13;
615
624
  const totalRawSize = items.reduce((sum, it) => sum + it.raw.length, 0);
616
625
  const payload = Buffer.allocUnsafe(headerSize + totalRawSize);
617
626
  payload[0] = modelByte;
618
627
  payload[1] = items.length;
619
- let offset = 2;
628
+ payload.writeUInt32LE(frameId >>> 0, 2);
629
+ let offset = 6;
620
630
  for (const item of items) {
621
631
  payload.writeUInt32LE(item.width, offset);
622
632
  payload.writeUInt32LE(item.height, offset + 4);
@@ -665,24 +675,48 @@ var PoolWorker = class {
665
675
  dispatch(msgType, payload) {
666
676
  const reqId = this.allocRequestId();
667
677
  return new Promise((resolve, reject) => {
678
+ const timer = this.armRequestTimeout(reqId, reject);
668
679
  this.pending.set(reqId, {
669
680
  resolve,
670
- reject
681
+ reject,
682
+ timer
671
683
  });
672
684
  try {
673
685
  this.writeFrame(reqId, msgType, payload);
674
686
  } catch (err) {
687
+ clearTimeout(timer);
675
688
  this.pending.delete(reqId);
676
689
  reject(err instanceof Error ? err : new Error(String(err)));
677
690
  }
678
691
  });
679
692
  }
693
+ /**
694
+ * Arm a watchdog that rejects a still-pending inference request after
695
+ * `POOL_INFER_TIMEOUT_MS`. `unref` so it never keeps the event loop alive.
696
+ * The normal response + `rejectAll` paths clear it via `PendingRequest.timer`.
697
+ */
698
+ armRequestTimeout(reqId, reject) {
699
+ const timer = setTimeout(() => {
700
+ if (this.pending.delete(reqId)) {
701
+ this.log.error("inference request timed out", { meta: {
702
+ worker: this.opts.workerLabel,
703
+ reqId,
704
+ timeoutMs: POOL_INFER_TIMEOUT_MS
705
+ } });
706
+ reject(/* @__PURE__ */ new Error(`PoolWorker[${this.opts.workerLabel}]: inference request ${reqId} timed out after ${POOL_INFER_TIMEOUT_MS}ms (worker alive, no reply)`));
707
+ }
708
+ }, POOL_INFER_TIMEOUT_MS);
709
+ timer.unref?.();
710
+ return timer;
711
+ }
680
712
  dispatchMulti(msgType, payloadLen, parts) {
681
713
  const reqId = this.allocRequestId();
682
714
  return new Promise((resolve, reject) => {
715
+ const timer = this.armRequestTimeout(reqId, reject);
683
716
  this.pending.set(reqId, {
684
717
  resolve,
685
- reject
718
+ reject,
719
+ timer
686
720
  });
687
721
  try {
688
722
  if (!this.process?.stdin) throw new Error("PoolWorker: not initialized");
@@ -693,6 +727,7 @@ var PoolWorker = class {
693
727
  this.process.stdin.write(prefix);
694
728
  for (const part of parts) this.process.stdin.write(part);
695
729
  } catch (err) {
730
+ clearTimeout(timer);
696
731
  this.pending.delete(reqId);
697
732
  reject(err instanceof Error ? err : new Error(String(err)));
698
733
  }
@@ -736,6 +771,7 @@ var PoolWorker = class {
736
771
  continue;
737
772
  }
738
773
  this.pending.delete(reqId);
774
+ if (entry.timer) clearTimeout(entry.timer);
739
775
  try {
740
776
  const parsed = JSON.parse(jsonBytes.toString("utf8"));
741
777
  entry.resolve(parsed);
@@ -747,7 +783,10 @@ var PoolWorker = class {
747
783
  rejectAll(err) {
748
784
  const entries = [...this.pending.values()];
749
785
  this.pending.clear();
750
- for (const entry of entries) entry.reject(err);
786
+ for (const entry of entries) {
787
+ if (entry.timer) clearTimeout(entry.timer);
788
+ entry.reject(err);
789
+ }
751
790
  }
752
791
  };
753
792
  var SharedInferencePool = class {
@@ -827,9 +866,9 @@ var SharedInferencePool = class {
827
866
  const w = this.pickWorker();
828
867
  return this.trackDroppedResponse(await w.inferRaw(this.encodeModelByte(modelIndex), raw, width, height, format), modelIndex);
829
868
  }
830
- async inferBatch(modelIndex, items) {
869
+ async inferBatch(modelIndex, items, frameId = 0) {
831
870
  if (items.length > 255) throw new Error(`SharedInferencePool.inferBatch: max 255 items per call, got ${items.length}`);
832
- return this.pickWorker().inferBatch(this.encodeModelByte(modelIndex), items);
871
+ return this.pickWorker().inferBatch(this.encodeModelByte(modelIndex), items, frameId);
833
872
  }
834
873
  async inferCached(modelIndex, frameId) {
835
874
  const w = this.pickWorker();
@@ -952,6 +991,8 @@ function serializeModelConfig(config) {
952
991
  confidence: config.confidence
953
992
  };
954
993
  if (config.nmsIouThreshold !== void 0) result["nmsIouThreshold"] = config.nmsIouThreshold;
994
+ if (config.inputNormalization !== void 0) result["inputNormalization"] = config.inputNormalization;
995
+ if (config.outputProbabilities !== void 0) result["outputProbabilities"] = config.outputProbabilities;
955
996
  if (config.inputWidth !== void 0) result["inputWidth"] = config.inputWidth;
956
997
  if (config.inputHeight !== void 0) result["inputHeight"] = config.inputHeight;
957
998
  if (config.inputChannels !== void 0) result["inputChannels"] = config.inputChannels;
@@ -1284,6 +1325,30 @@ var OBJECT_DETECTION_POOL_CONFIDENCE_FLOOR = .05;
1284
1325
  function resolvePoolConfidenceFloor(stepId, defaultConfidence) {
1285
1326
  return stepId === OBJECT_DETECTION_STEP_ID ? OBJECT_DETECTION_POOL_CONFIDENCE_FLOOR : defaultConfidence;
1286
1327
  }
1328
+ /**
1329
+ * Load a model's runtime label list from its `-labels.json` extraFile, if any.
1330
+ *
1331
+ * Accepts BOTH on-disk shapes:
1332
+ * - a flat `string[]` (legacy label files), and
1333
+ * - a `{ "labels": string[], ... }` metadata object (the newer format the
1334
+ * animal / bird / vehicle classifiers ship, which also carries preprocess /
1335
+ * license metadata alongside the label array).
1336
+ *
1337
+ * Returns `undefined` when the model declares no `-labels.json`, the file is
1338
+ * absent, or its contents are not a recognised label shape — the caller then
1339
+ * falls back to the step's `def.labels`.
1340
+ */
1341
+ var LabelsFileSchema = require_dist.union([require_dist.array(require_dist.string()).readonly(), require_dist.object({ labels: require_dist.array(require_dist.string()).readonly() })]);
1342
+ function readModelLabels(modelsDir, modelEntry) {
1343
+ const labelsFile = modelEntry.extraFiles?.find((f) => f.filename.endsWith("-labels.json"));
1344
+ if (!labelsFile) return void 0;
1345
+ const labelsPath = node_path.join(modelsDir, labelsFile.filename);
1346
+ if (!node_fs.existsSync(labelsPath)) return void 0;
1347
+ const parsed = LabelsFileSchema.safeParse(JSON.parse(node_fs.readFileSync(labelsPath, "utf8")));
1348
+ if (!parsed.success) return void 0;
1349
+ const data = parsed.data;
1350
+ return "labels" in data ? data.labels : data;
1351
+ }
1287
1352
  var EngineFactory = class {
1288
1353
  pool = null;
1289
1354
  poolManager = null;
@@ -1361,11 +1426,11 @@ var EngineFactory = class {
1361
1426
  * Used by `runPipelineBatch` for fair batched benchmarking — N frames
1362
1427
  * in one IPC round-trip, amortising the per-call envelope overhead.
1363
1428
  */
1364
- async batchInferRaw(stepId, items, modelId) {
1429
+ async batchInferRaw(stepId, items, modelId, frameId = 0) {
1365
1430
  if (!this.poolManager) throw new Error("EngineFactory.batchInferRaw: pool not initialized");
1366
1431
  const idx = this.poolManager.getPoolIndex(stepId, modelId);
1367
1432
  if (idx === null) throw new Error(`EngineFactory.batchInferRaw: step "${stepId}"${modelId ? ` (model "${modelId}")` : ""} is not loaded`);
1368
- return this.poolManager.getPool().inferBatch(idx, items);
1433
+ return this.poolManager.getPool().inferBatch(idx, items, frameId);
1369
1434
  }
1370
1435
  async cacheFrameInPool(raw, width, height, format) {
1371
1436
  if (!this.poolManager) throw new Error("cacheFrameInPool: pool not available");
@@ -1451,14 +1516,7 @@ var EngineFactory = class {
1451
1516
  const inputHeight = modelEntry.inputSize.height;
1452
1517
  const inputSize = Math.max(inputWidth, inputHeight);
1453
1518
  const inputChannels = modelEntry.inputChannels ?? 3;
1454
- let labels = def.labels;
1455
- if (!labels && modelEntry.extraFiles) {
1456
- const labelsFile = modelEntry.extraFiles.find((f) => f.filename.endsWith("-labels.json"));
1457
- if (labelsFile) {
1458
- const labelsPath = node_path.join(this.opts.modelsDir, labelsFile.filename);
1459
- if (node_fs.existsSync(labelsPath)) labels = JSON.parse(node_fs.readFileSync(labelsPath, "utf8"));
1460
- }
1461
- }
1519
+ const labels = readModelLabels(this.opts.modelsDir, modelEntry) ?? def.labels;
1462
1520
  return {
1463
1521
  path: modelPath,
1464
1522
  inputSize,
@@ -1466,6 +1524,8 @@ var EngineFactory = class {
1466
1524
  inputHeight,
1467
1525
  inputChannels,
1468
1526
  preprocessMode: modelEntry.preprocessMode ?? "letterbox",
1527
+ inputNormalization: modelEntry.inputNormalization,
1528
+ outputProbabilities: modelEntry.outputProbabilities,
1469
1529
  postprocessor: modelEntry.postprocessor ?? def.postprocessor,
1470
1530
  confidence: resolvePoolConfidenceFloor(stepId, def.defaultConfidence),
1471
1531
  nmsIouThreshold: this.opts.nmsIouThreshold ?? .45,
@@ -2584,7 +2644,8 @@ function applyChildOutput(parent, childStep, output, stepLatencyMs, ctx) {
2584
2644
  case "classifications": {
2585
2645
  const rawEnabled = childStep.settings?.["enabledClasses"];
2586
2646
  const enabledSet = Array.isArray(rawEnabled) && rawEnabled.length > 0 ? new Set(rawEnabled.filter((v) => typeof v === "string")) : null;
2587
- const passesClass = (c) => enabledSet === null || enabledSet.has(c.class);
2647
+ const rejectSet = childStep.definition.rejectClasses && childStep.definition.rejectClasses.length > 0 ? new Set(childStep.definition.rejectClasses.map((c) => c.toLowerCase())) : /* @__PURE__ */ new Set();
2648
+ const passesClass = (c) => (enabledSet === null || enabledSet.has(c.class)) && !rejectSet.has(c.class.toLowerCase());
2588
2649
  const minConfidence = typeof childStep.settings?.["minConfidence"] === "number" ? childStep.settings["minConfidence"] : 0;
2589
2650
  const winner = output.classifications[0];
2590
2651
  if (winner && passesClass(winner) && winner.score >= minConfidence) parent.ownLabels.push({
@@ -5551,7 +5612,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5551
5612
  totalRawBytes: items.reduce((s, it) => s + it.raw.length, 0)
5552
5613
  } });
5553
5614
  const start = performance.now();
5554
- const rawResults = await factory.batchInferRaw(rootStep.addonId, items, rootStep.modelId);
5615
+ const rawResults = await factory.batchInferRaw(rootStep.addonId, items, rootStep.modelId, input.frameId ?? 0);
5555
5616
  const totalMs = performance.now() - start;
5556
5617
  this.log.info("runPipelineBatch fast path returned", { meta: {
5557
5618
  phase: "batch",
@@ -1,6 +1,6 @@
1
1
  import { t as __require } from "../chunk-BdkLduGY.mjs";
2
- import { $ as sleep, A as pipelineExecutorCapability, B as errMsg, C as evaluateZoneRules, F as runtimeDevices$1, G as createEvent, K as hydrateSchema, R as supportedRuntimes$1, S as detectionPipelineCapability, V as BaseAddon, X as parseJsonUnknown, b as defaultDeviceFor$1, dt as EventCategory, k as nodePin, m as YAMNET_TO_MACRO, t as APPLE_SA_TO_MACRO } from "../dist-D6tHM9GA.mjs";
3
- import { a as getStepDefinition, i as getStep, n as ALL_STEPS, o as resolveModelForFormat, r as getDefaultModelForFormat, t as ALL_PIPELINE_STEPS } from "../step-definitions-Dbsm64_I.mjs";
2
+ import { $ as sleep, A as pipelineExecutorCapability, B as errMsg, C as evaluateZoneRules, F as runtimeDevices$1, G as createEvent, K as hydrateSchema, R as supportedRuntimes$1, S as detectionPipelineCapability, V as BaseAddon, X as parseJsonUnknown, b as defaultDeviceFor$1, dt as EventCategory, k as nodePin, lt as string, m as YAMNET_TO_MACRO, st as object, t as APPLE_SA_TO_MACRO, tt as array, ut as union } from "../dist-DkGHtAaK.mjs";
3
+ import { a as getStepDefinition, i as getStep, n as ALL_STEPS, o as resolveModelForFormat, r as getDefaultModelForFormat, t as ALL_PIPELINE_STEPS } from "../step-definitions-Delhq28w.mjs";
4
4
  import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
5
5
  import { a as ensureModel, o as isModelDownloaded, r as deleteModelFromDisk } from "../model-download-service-Cp9f4dk6-CwUb5v3Y.mjs";
6
6
  import { spawn } from "node:child_process";
@@ -418,6 +418,15 @@ var RAW_FMT_CODE = {
418
418
  bgr: 1,
419
419
  gray: 2
420
420
  };
421
+ /**
422
+ * Per-inference-request reply timeout (ms). Turns a wedged request (worker
423
+ * alive but no reply) into a rejection so every caller settles — runtime frame
424
+ * dispatch drops the frame; the benchmark full-tree run rejects the wedged
425
+ * child (its enclosing try/catch keeps the root detections) instead of hanging
426
+ * the UI on "loading" forever. Generous so legitimate slow inference (cold JIT,
427
+ * large frame) never trips; override via env for constrained hardware.
428
+ */
429
+ var POOL_INFER_TIMEOUT_MS = Math.max(1e3, Number(process.env["CAMSTACK_POOL_INFER_TIMEOUT_MS"]) || 6e4);
421
430
  var PoolHandle = class {
422
431
  pool;
423
432
  modelIndex;
@@ -600,15 +609,16 @@ var PoolWorker = class {
600
609
  rawHeader[9] = fmtCode;
601
610
  return this.dispatchMulti(MSG_INFER_RAW, rawHeader.length + raw.length, [rawHeader, raw]);
602
611
  }
603
- async inferBatch(modelByte, items) {
612
+ async inferBatch(modelByte, items, frameId = 0) {
604
613
  this.ensureReady();
605
614
  if (items.length === 0) return [];
606
- const headerSize = 2 + items.length * 13;
615
+ const headerSize = 6 + items.length * 13;
607
616
  const totalRawSize = items.reduce((sum, it) => sum + it.raw.length, 0);
608
617
  const payload = Buffer.allocUnsafe(headerSize + totalRawSize);
609
618
  payload[0] = modelByte;
610
619
  payload[1] = items.length;
611
- let offset = 2;
620
+ payload.writeUInt32LE(frameId >>> 0, 2);
621
+ let offset = 6;
612
622
  for (const item of items) {
613
623
  payload.writeUInt32LE(item.width, offset);
614
624
  payload.writeUInt32LE(item.height, offset + 4);
@@ -657,24 +667,48 @@ var PoolWorker = class {
657
667
  dispatch(msgType, payload) {
658
668
  const reqId = this.allocRequestId();
659
669
  return new Promise((resolve, reject) => {
670
+ const timer = this.armRequestTimeout(reqId, reject);
660
671
  this.pending.set(reqId, {
661
672
  resolve,
662
- reject
673
+ reject,
674
+ timer
663
675
  });
664
676
  try {
665
677
  this.writeFrame(reqId, msgType, payload);
666
678
  } catch (err) {
679
+ clearTimeout(timer);
667
680
  this.pending.delete(reqId);
668
681
  reject(err instanceof Error ? err : new Error(String(err)));
669
682
  }
670
683
  });
671
684
  }
685
+ /**
686
+ * Arm a watchdog that rejects a still-pending inference request after
687
+ * `POOL_INFER_TIMEOUT_MS`. `unref` so it never keeps the event loop alive.
688
+ * The normal response + `rejectAll` paths clear it via `PendingRequest.timer`.
689
+ */
690
+ armRequestTimeout(reqId, reject) {
691
+ const timer = setTimeout(() => {
692
+ if (this.pending.delete(reqId)) {
693
+ this.log.error("inference request timed out", { meta: {
694
+ worker: this.opts.workerLabel,
695
+ reqId,
696
+ timeoutMs: POOL_INFER_TIMEOUT_MS
697
+ } });
698
+ reject(/* @__PURE__ */ new Error(`PoolWorker[${this.opts.workerLabel}]: inference request ${reqId} timed out after ${POOL_INFER_TIMEOUT_MS}ms (worker alive, no reply)`));
699
+ }
700
+ }, POOL_INFER_TIMEOUT_MS);
701
+ timer.unref?.();
702
+ return timer;
703
+ }
672
704
  dispatchMulti(msgType, payloadLen, parts) {
673
705
  const reqId = this.allocRequestId();
674
706
  return new Promise((resolve, reject) => {
707
+ const timer = this.armRequestTimeout(reqId, reject);
675
708
  this.pending.set(reqId, {
676
709
  resolve,
677
- reject
710
+ reject,
711
+ timer
678
712
  });
679
713
  try {
680
714
  if (!this.process?.stdin) throw new Error("PoolWorker: not initialized");
@@ -685,6 +719,7 @@ var PoolWorker = class {
685
719
  this.process.stdin.write(prefix);
686
720
  for (const part of parts) this.process.stdin.write(part);
687
721
  } catch (err) {
722
+ clearTimeout(timer);
688
723
  this.pending.delete(reqId);
689
724
  reject(err instanceof Error ? err : new Error(String(err)));
690
725
  }
@@ -728,6 +763,7 @@ var PoolWorker = class {
728
763
  continue;
729
764
  }
730
765
  this.pending.delete(reqId);
766
+ if (entry.timer) clearTimeout(entry.timer);
731
767
  try {
732
768
  const parsed = JSON.parse(jsonBytes.toString("utf8"));
733
769
  entry.resolve(parsed);
@@ -739,7 +775,10 @@ var PoolWorker = class {
739
775
  rejectAll(err) {
740
776
  const entries = [...this.pending.values()];
741
777
  this.pending.clear();
742
- for (const entry of entries) entry.reject(err);
778
+ for (const entry of entries) {
779
+ if (entry.timer) clearTimeout(entry.timer);
780
+ entry.reject(err);
781
+ }
743
782
  }
744
783
  };
745
784
  var SharedInferencePool = class {
@@ -819,9 +858,9 @@ var SharedInferencePool = class {
819
858
  const w = this.pickWorker();
820
859
  return this.trackDroppedResponse(await w.inferRaw(this.encodeModelByte(modelIndex), raw, width, height, format), modelIndex);
821
860
  }
822
- async inferBatch(modelIndex, items) {
861
+ async inferBatch(modelIndex, items, frameId = 0) {
823
862
  if (items.length > 255) throw new Error(`SharedInferencePool.inferBatch: max 255 items per call, got ${items.length}`);
824
- return this.pickWorker().inferBatch(this.encodeModelByte(modelIndex), items);
863
+ return this.pickWorker().inferBatch(this.encodeModelByte(modelIndex), items, frameId);
825
864
  }
826
865
  async inferCached(modelIndex, frameId) {
827
866
  const w = this.pickWorker();
@@ -944,6 +983,8 @@ function serializeModelConfig(config) {
944
983
  confidence: config.confidence
945
984
  };
946
985
  if (config.nmsIouThreshold !== void 0) result["nmsIouThreshold"] = config.nmsIouThreshold;
986
+ if (config.inputNormalization !== void 0) result["inputNormalization"] = config.inputNormalization;
987
+ if (config.outputProbabilities !== void 0) result["outputProbabilities"] = config.outputProbabilities;
947
988
  if (config.inputWidth !== void 0) result["inputWidth"] = config.inputWidth;
948
989
  if (config.inputHeight !== void 0) result["inputHeight"] = config.inputHeight;
949
990
  if (config.inputChannels !== void 0) result["inputChannels"] = config.inputChannels;
@@ -1276,6 +1317,30 @@ var OBJECT_DETECTION_POOL_CONFIDENCE_FLOOR = .05;
1276
1317
  function resolvePoolConfidenceFloor(stepId, defaultConfidence) {
1277
1318
  return stepId === OBJECT_DETECTION_STEP_ID ? OBJECT_DETECTION_POOL_CONFIDENCE_FLOOR : defaultConfidence;
1278
1319
  }
1320
+ /**
1321
+ * Load a model's runtime label list from its `-labels.json` extraFile, if any.
1322
+ *
1323
+ * Accepts BOTH on-disk shapes:
1324
+ * - a flat `string[]` (legacy label files), and
1325
+ * - a `{ "labels": string[], ... }` metadata object (the newer format the
1326
+ * animal / bird / vehicle classifiers ship, which also carries preprocess /
1327
+ * license metadata alongside the label array).
1328
+ *
1329
+ * Returns `undefined` when the model declares no `-labels.json`, the file is
1330
+ * absent, or its contents are not a recognised label shape — the caller then
1331
+ * falls back to the step's `def.labels`.
1332
+ */
1333
+ var LabelsFileSchema = union([array(string()).readonly(), object({ labels: array(string()).readonly() })]);
1334
+ function readModelLabels(modelsDir, modelEntry) {
1335
+ const labelsFile = modelEntry.extraFiles?.find((f) => f.filename.endsWith("-labels.json"));
1336
+ if (!labelsFile) return void 0;
1337
+ const labelsPath = path$1.join(modelsDir, labelsFile.filename);
1338
+ if (!fs.existsSync(labelsPath)) return void 0;
1339
+ const parsed = LabelsFileSchema.safeParse(JSON.parse(fs.readFileSync(labelsPath, "utf8")));
1340
+ if (!parsed.success) return void 0;
1341
+ const data = parsed.data;
1342
+ return "labels" in data ? data.labels : data;
1343
+ }
1279
1344
  var EngineFactory = class {
1280
1345
  pool = null;
1281
1346
  poolManager = null;
@@ -1353,11 +1418,11 @@ var EngineFactory = class {
1353
1418
  * Used by `runPipelineBatch` for fair batched benchmarking — N frames
1354
1419
  * in one IPC round-trip, amortising the per-call envelope overhead.
1355
1420
  */
1356
- async batchInferRaw(stepId, items, modelId) {
1421
+ async batchInferRaw(stepId, items, modelId, frameId = 0) {
1357
1422
  if (!this.poolManager) throw new Error("EngineFactory.batchInferRaw: pool not initialized");
1358
1423
  const idx = this.poolManager.getPoolIndex(stepId, modelId);
1359
1424
  if (idx === null) throw new Error(`EngineFactory.batchInferRaw: step "${stepId}"${modelId ? ` (model "${modelId}")` : ""} is not loaded`);
1360
- return this.poolManager.getPool().inferBatch(idx, items);
1425
+ return this.poolManager.getPool().inferBatch(idx, items, frameId);
1361
1426
  }
1362
1427
  async cacheFrameInPool(raw, width, height, format) {
1363
1428
  if (!this.poolManager) throw new Error("cacheFrameInPool: pool not available");
@@ -1443,14 +1508,7 @@ var EngineFactory = class {
1443
1508
  const inputHeight = modelEntry.inputSize.height;
1444
1509
  const inputSize = Math.max(inputWidth, inputHeight);
1445
1510
  const inputChannels = modelEntry.inputChannels ?? 3;
1446
- let labels = def.labels;
1447
- if (!labels && modelEntry.extraFiles) {
1448
- const labelsFile = modelEntry.extraFiles.find((f) => f.filename.endsWith("-labels.json"));
1449
- if (labelsFile) {
1450
- const labelsPath = path$1.join(this.opts.modelsDir, labelsFile.filename);
1451
- if (fs.existsSync(labelsPath)) labels = JSON.parse(fs.readFileSync(labelsPath, "utf8"));
1452
- }
1453
- }
1511
+ const labels = readModelLabels(this.opts.modelsDir, modelEntry) ?? def.labels;
1454
1512
  return {
1455
1513
  path: modelPath,
1456
1514
  inputSize,
@@ -1458,6 +1516,8 @@ var EngineFactory = class {
1458
1516
  inputHeight,
1459
1517
  inputChannels,
1460
1518
  preprocessMode: modelEntry.preprocessMode ?? "letterbox",
1519
+ inputNormalization: modelEntry.inputNormalization,
1520
+ outputProbabilities: modelEntry.outputProbabilities,
1461
1521
  postprocessor: modelEntry.postprocessor ?? def.postprocessor,
1462
1522
  confidence: resolvePoolConfidenceFloor(stepId, def.defaultConfidence),
1463
1523
  nmsIouThreshold: this.opts.nmsIouThreshold ?? .45,
@@ -2576,7 +2636,8 @@ function applyChildOutput(parent, childStep, output, stepLatencyMs, ctx) {
2576
2636
  case "classifications": {
2577
2637
  const rawEnabled = childStep.settings?.["enabledClasses"];
2578
2638
  const enabledSet = Array.isArray(rawEnabled) && rawEnabled.length > 0 ? new Set(rawEnabled.filter((v) => typeof v === "string")) : null;
2579
- const passesClass = (c) => enabledSet === null || enabledSet.has(c.class);
2639
+ const rejectSet = childStep.definition.rejectClasses && childStep.definition.rejectClasses.length > 0 ? new Set(childStep.definition.rejectClasses.map((c) => c.toLowerCase())) : /* @__PURE__ */ new Set();
2640
+ const passesClass = (c) => (enabledSet === null || enabledSet.has(c.class)) && !rejectSet.has(c.class.toLowerCase());
2580
2641
  const minConfidence = typeof childStep.settings?.["minConfidence"] === "number" ? childStep.settings["minConfidence"] : 0;
2581
2642
  const winner = output.classifications[0];
2582
2643
  if (winner && passesClass(winner) && winner.score >= minConfidence) parent.ownLabels.push({
@@ -5543,7 +5604,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5543
5604
  totalRawBytes: items.reduce((s, it) => s + it.raw.length, 0)
5544
5605
  } });
5545
5606
  const start = performance.now();
5546
- const rawResults = await factory.batchInferRaw(rootStep.addonId, items, rootStep.modelId);
5607
+ const rawResults = await factory.batchInferRaw(rootStep.addonId, items, rootStep.modelId, input.frameId ?? 0);
5547
5608
  const totalMs = performance.now() - start;
5548
5609
  this.log.info("runPipelineBatch fast path returned", { meta: {
5549
5610
  phase: "batch",
@@ -7207,6 +7207,17 @@ var ModelCatalogEntrySchema = object({
7207
7207
  "imagenet",
7208
7208
  "none"
7209
7209
  ]).optional(),
7210
+ /**
7211
+ * The model already applies softmax IN-GRAPH — its raw output is a
7212
+ * probability distribution, not logits. When set, the `softmax`
7213
+ * postprocessor must NOT re-apply softmax: re-softmaxing an already-normalised
7214
+ * probability vector collapses it toward uniform (top-1 score craters far
7215
+ * below its true value, making every confidence gate meaningless). Absent ⇒
7216
+ * the output is raw logits and the postprocessor applies softmax (the normal
7217
+ * case). Set on the Google AIY Birds `bird-classifier` (softmax baked into the
7218
+ * TF graph). Threaded to the Python pool via `PoolModelConfig.outputProbabilities`.
7219
+ */
7220
+ outputProbabilities: boolean().optional(),
7210
7221
  preprocessMode: _enum(["letterbox", "resize"]).optional(),
7211
7222
  /**
7212
7223
  * Per-MODEL postprocessor override. Absent ⇒ the step's own
@@ -12081,7 +12092,14 @@ var pipelineExecutorCapability = {
12081
12092
  * (inputClasses ≠ null) are skipped and served per-track via
12082
12093
  * pipelineRunner.runDetailSubtree (two-plane design).
12083
12094
  */
12084
- plane: _enum(["full", "frame"]).optional()
12095
+ plane: _enum(["full", "frame"]).optional(),
12096
+ /**
12097
+ * Inference-device selector (Phase 2 multi-device). Format
12098
+ * `<backend>:<device>` (e.g. `openvino:gpu`, `edgetpu:usb`, `cpu`).
12099
+ * Omitted ⇒ the runner's default device (current single-engine
12100
+ * behaviour). Selects WHICH device pool of the node runs the call.
12101
+ */
12102
+ deviceKey: string().optional()
12085
12103
  }), PipelineRunResultBridge, { kind: "mutation" }),
12086
12104
  /**
12087
12105
  * Batched run — N raw frames packed into one cap call. The provider
@@ -12099,7 +12117,20 @@ var pipelineExecutorCapability = {
12099
12117
  steps: array(PipelineStepInputSchema).min(1),
12100
12118
  frames: array(FrameInputSchema).min(1).max(255),
12101
12119
  deviceId: number().optional(),
12102
- sessionId: string().optional()
12120
+ sessionId: string().optional(),
12121
+ /**
12122
+ * Pure-inference benchmark hint. A NONZERO uint32 pins every frame in
12123
+ * the batch to the Python pool's bench preprocess cache
12124
+ * (`_bench_frame_id`) so a REPEATED benchmark frame is decoded +
12125
+ * preprocessed ONCE and every later inference is a pure-inference cache
12126
+ * hit — the sustained-throughput run measures inference, not
12127
+ * decode+preprocess+infer. Omitted/0 for live frames (all different →
12128
+ * full preprocess every call, correct). Fresh per sustained run;
12129
+ * released via `uncacheFrame`.
12130
+ */
12131
+ frameId: number().int().nonnegative().optional(),
12132
+ /** Inference-device selector (Phase 2 multi-device); see runPipeline. */
12133
+ deviceKey: string().optional()
12103
12134
  }), object({ results: array(PipelineRunResultBridge).readonly() }), { kind: "mutation" }),
12104
12135
  /**
12105
12136
  * Cache a raw frame inside the Python inference pool's memory.
@@ -12516,7 +12547,14 @@ var RunnerCameraConfigSchema = object({
12516
12547
  * camera's detect node differs from its source-owner (P2d, gated by the
12517
12548
  * `remoteSourcingNodes` rollout setting).
12518
12549
  */
12519
- frameSource: RunnerFrameSourceSchema.default({ kind: "local-broker" })
12550
+ frameSource: RunnerFrameSourceSchema.default({ kind: "local-broker" }),
12551
+ /**
12552
+ * Inference-device selector for this camera's sessions (Phase 2 multi-device).
12553
+ * Format `<backend>:<device>` (e.g. `openvino:gpu`, `edgetpu:usb`, `cpu`);
12554
+ * omitted ⇒ the runner's default device. The engine itself stays node-local —
12555
+ * this only selects WHICH device pool of that node runs the session.
12556
+ */
12557
+ deviceKey: string().optional()
12520
12558
  });
12521
12559
  motionFpsField.min, motionFpsField.max, motionFpsField.step, motionFpsField.default, detectionFpsField.min, detectionFpsField.max, detectionFpsField.step, detectionFpsField.default, motionCooldownMsField.min, motionCooldownMsField.max, motionCooldownMsField.step, motionCooldownMsField.default, occupancyRecheckSecField.min, occupancyRecheckSecField.max, occupancyRecheckSecField.step, occupancyRecheckSecField.default, occupancyRecheckFramesField.min, occupancyRecheckFramesField.max, occupancyRecheckFramesField.step, occupancyRecheckFramesField.default;
12522
12560
  /**
@@ -14087,6 +14125,8 @@ method(_void(), _void(), { kind: "mutation" }), method(_void(), _void(), { kind:
14087
14125
  kind: "mutation",
14088
14126
  auth: "admin"
14089
14127
  });
14128
+ DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder;
14129
+ new Set(Object.values(DeviceType));
14090
14130
  /**
14091
14131
  * Evaluate `rules` against `items`. Returns the items partitioned
14092
14132
  * into `passed` (kept by the gate) and `excluded` (dropped). When
@@ -21000,17 +21040,28 @@ var PlatformScoreSchema = object({
21000
21040
  format: _enum([
21001
21041
  "onnx",
21002
21042
  "coreml",
21003
- "openvino"
21043
+ "openvino",
21044
+ "tflite"
21004
21045
  ]),
21005
21046
  score: number(),
21006
21047
  reason: string(),
21007
21048
  available: boolean()
21008
21049
  });
21050
+ var InferenceDeviceDescriptorSchema = object({
21051
+ key: string(),
21052
+ backend: string(),
21053
+ device: string(),
21054
+ format: ModelFormatSchema,
21055
+ runtime: literal("python"),
21056
+ score: number(),
21057
+ available: boolean()
21058
+ });
21009
21059
  var PlatformCapabilitiesSchema = object({
21010
21060
  hardware: HardwareInfoSchema,
21011
21061
  scores: array(PlatformScoreSchema).readonly(),
21012
21062
  bestScore: PlatformScoreSchema,
21013
- pythonPath: string().nullable()
21063
+ pythonPath: string().nullable(),
21064
+ devices: array(InferenceDeviceDescriptorSchema).readonly()
21014
21065
  });
21015
21066
  var ModelRequirementSchema = object({
21016
21067
  modelId: string(),