@camstack/addon-pipeline 1.2.231 → 1.2.233

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 (32) hide show
  1. package/dist/audio-analyzer/index.js +2 -2
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/{default-detection-model-C7fsHtHQ.js → default-detection-model-CDhXxOJQ.js} +1 -1
  4. package/dist/{default-detection-model-DC4yVaFM.mjs → default-detection-model-I8kuDTEi.mjs} +1 -1
  5. package/dist/detection-pipeline/index.js +4 -4
  6. package/dist/detection-pipeline/index.mjs +4 -4
  7. package/dist/{dist-DTPYOq7J.mjs → dist-But4XMDJ.mjs} +107 -6
  8. package/dist/{dist-p5lZW8pr.js → dist-NX0e_oWp.js} +107 -6
  9. package/dist/motion-wasm/index.js +1 -1
  10. package/dist/motion-wasm/index.mjs +1 -1
  11. package/dist/{node-Cz92fVGW.mjs → node-BHxRrsoX.mjs} +1 -1
  12. package/dist/{node-CGVUk5S9.js → node-BgM4n8D4.js} +1 -1
  13. package/dist/pipeline-runner/index.js +205 -17
  14. package/dist/pipeline-runner/index.mjs +205 -17
  15. package/dist/{process-memory-BwijtYhM.mjs → process-memory-C3-EOjjf.mjs} +1 -1
  16. package/dist/{process-memory-COypUeEc.js → process-memory-C5_Mo9TI.js} +1 -1
  17. package/dist/recorder/index.js +3 -2
  18. package/dist/recorder/index.mjs +3 -2
  19. package/dist/{segment-demux-js-B-yzHYVc.js → segment-demux-js-BAD7i9RI.js} +1 -1
  20. package/dist/{segment-demux-js-CyaNDtrw.mjs → segment-demux-js-CVZACSOh.mjs} +1 -1
  21. package/dist/stream-broker/_stub.js +2 -2
  22. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-B9KO-igY.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DN-9qAgC.mjs} +2 -2
  23. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D3XkHB88.mjs +26 -0
  24. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-JoG-yDpV.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DDCieNDB.mjs} +1 -1
  25. package/dist/stream-broker/demux-worker-child.js +1 -1
  26. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  27. package/dist/stream-broker/{hostInit-CDwsQyIR.mjs → hostInit-CT06n_gA.mjs} +2 -2
  28. package/dist/stream-broker/index.js +3 -3
  29. package/dist/stream-broker/index.mjs +3 -3
  30. package/dist/stream-broker/remoteEntry.js +1 -1
  31. package/package.json +1 -1
  32. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-mnvO611B.mjs +0 -26
@@ -3,9 +3,9 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-emK7D4bc.js");
6
- const require_dist = require("../dist-p5lZW8pr.js");
7
- const require_node = require("../node-CGVUk5S9.js");
8
- const require_default_detection_model = require("../default-detection-model-C7fsHtHQ.js");
6
+ const require_dist = require("../dist-NX0e_oWp.js");
7
+ const require_node = require("../node-BgM4n8D4.js");
8
+ const require_default_detection_model = require("../default-detection-model-CDhXxOJQ.js");
9
9
  const require_worker_protocol = require("../worker-protocol-B2MfQLlu.js");
10
10
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
11
11
  const require_lazy_sharp = require("../lazy-sharp-BuwKlQLR.js");
@@ -2096,6 +2096,73 @@ function startSessionDecodePump(deps) {
2096
2096
  iterator.return().catch(() => {});
2097
2097
  };
2098
2098
  }
2099
+ /**
2100
+ * Read the camera's `detectionSources`.
2101
+ *
2102
+ * ABSENT is the pipeline — a runner or an attach older than the field behaves
2103
+ * exactly as it always did. EMPTY is not the same thing and must never be
2104
+ * widened into "everything": it is a camera an operator left with no root
2105
+ * detector, and inventing one for them is how a default acts without anybody
2106
+ * choosing it.
2107
+ */
2108
+ function planDetectionSources(sources) {
2109
+ if (sources === void 0) return {
2110
+ runsPipeline: true,
2111
+ runsOnboard: false
2112
+ };
2113
+ return {
2114
+ runsPipeline: sources.includes("pipeline"),
2115
+ runsOnboard: sources.includes("onboard")
2116
+ };
2117
+ }
2118
+ /** Clamp a denormalised box into the frame; a firmware box may exceed it. */
2119
+ function clamp(value, max) {
2120
+ return Math.max(0, Math.min(value, max));
2121
+ }
2122
+ /**
2123
+ * Pair the camera's last detections with a decoded frame, in the FRAME's pixel
2124
+ * space.
2125
+ *
2126
+ * The old converter denormalised against a nominal 1000×1000 when the push
2127
+ * carried no dimensions, and documented that consumers should treat a native
2128
+ * bbox as relative. That was survivable while nothing cut pixels from it. Here
2129
+ * the boxes travel with a real frame and a real handle, so they are scaled to
2130
+ * that frame — the same space every other detection on this plane lives in.
2131
+ */
2132
+ function onboardRootDetections(slot, frame) {
2133
+ if (slot === null || slot.detections.length === 0) return { kind: "none" };
2134
+ const ageMs = Math.abs(frame.timestamp - slot.at);
2135
+ if (ageMs > 1e3) return {
2136
+ kind: "expired",
2137
+ ageMs
2138
+ };
2139
+ const boxed = slot.detections.filter((d) => d.bbox != null);
2140
+ if (boxed.length === 0) return {
2141
+ kind: "no-geometry",
2142
+ classes: slot.detections.map((d) => d.class)
2143
+ };
2144
+ return {
2145
+ kind: "paired",
2146
+ detections: boxed.map((d, i) => {
2147
+ const [nx, ny, nw, nh] = d.bbox;
2148
+ const x = clamp(nx * frame.width, frame.width);
2149
+ const y = clamp(ny * frame.height, frame.height);
2150
+ return {
2151
+ id: `onboard-${String(slot.at)}-${String(i)}`,
2152
+ kind: "first-level",
2153
+ macroClass: d.class,
2154
+ score: d.confidence ?? 1,
2155
+ labels: [],
2156
+ bbox: {
2157
+ x,
2158
+ y,
2159
+ width: clamp(nw * frame.width, frame.width - x),
2160
+ height: clamp(nh * frame.height, frame.height - y)
2161
+ }
2162
+ };
2163
+ })
2164
+ };
2165
+ }
2099
2166
  //#endregion
2100
2167
  //#region src/pipeline-runner/bench-step-schema.ts
2101
2168
  /**
@@ -5644,6 +5711,8 @@ var PipelineRunner = class {
5644
5711
  cameras = /* @__PURE__ */ new Map();
5645
5712
  semaphore;
5646
5713
  resultCallbacks = [];
5714
+ /** Last report per `deviceId:reason` for {@link noteOnboardRootIdle}. */
5715
+ onboardIdleReportedAt = /* @__PURE__ */ new Map();
5647
5716
  defaultRoundRobinKeys = [];
5648
5717
  defaultRoundRobinIndex = 0;
5649
5718
  intervalHandle = null;
@@ -5724,6 +5793,7 @@ var PipelineRunner = class {
5724
5793
  const initialPhase = registration.detectionMode === "disabled" ? "idle" : registration.detectionMode === "always-on" ? "active" : "watching";
5725
5794
  const state = {
5726
5795
  registration,
5796
+ onboardSlot: null,
5727
5797
  motionQueue,
5728
5798
  detectionQueue,
5729
5799
  preRollQueue: new PreRollLane({
@@ -6200,6 +6270,15 @@ var PipelineRunner = class {
6200
6270
  const { frame, handle } = entry;
6201
6271
  state.inflight += 1;
6202
6272
  const enqueuedAt = frame._enqueuedAt ?? pickedAt;
6273
+ const plan = planDetectionSources(state.registration.detectionSources);
6274
+ if (!plan.runsPipeline) {
6275
+ try {
6276
+ await this.processOnboardRoot(deviceId, entry, state, streamType, plan);
6277
+ } finally {
6278
+ state.inflight -= 1;
6279
+ }
6280
+ return;
6281
+ }
6203
6282
  const rawRelease = await this.semaphore.acquire();
6204
6283
  let released = false;
6205
6284
  const release = () => {
@@ -6223,7 +6302,7 @@ var PipelineRunner = class {
6223
6302
  state.processedCount++;
6224
6303
  if (result) {
6225
6304
  if (result.shed === true) state.shedFrames++;
6226
- await this.notifyCallbacks(deviceId, frame, result, streamType, handle);
6305
+ await this.notifyCallbacks(deviceId, frame, result, streamType, handle, "pipeline");
6227
6306
  frameReleaseReason = "success";
6228
6307
  const emittedAt = Date.now();
6229
6308
  const capturedAt = frame.capturedAt;
@@ -6250,6 +6329,96 @@ var PipelineRunner = class {
6250
6329
  }
6251
6330
  }
6252
6331
  /**
6332
+ * The CAMERA is this camera's root step.
6333
+ *
6334
+ * Same shape as the pipeline path and deliberately so: one decoded frame, one
6335
+ * `FrameResult`, the same `notifyCallbacks`, the same handle, the same
6336
+ * retention release. Everything downstream — subject tiles, birth evidence,
6337
+ * the `detailSteps` announce, the overlay, the tracker — cannot tell which
6338
+ * detector produced the boxes, which is the entire point. What it does NOT do
6339
+ * is take an inference permit: the accelerator is never asked.
6340
+ *
6341
+ * A frame with no fresh onboard detection produces NO result rather than an
6342
+ * empty one. An empty `FrameResult` is a positive statement ("I looked and
6343
+ * there was nothing"), and this path did not look — it decoded a frame while
6344
+ * the camera happened to be silent. Emitting emptiness would tell the
6345
+ * tracker every silent frame is evidence of absence and coast every subject
6346
+ * to death between two pushes.
6347
+ */
6348
+ async processOnboardRoot(deviceId, entry, state, streamType, plan) {
6349
+ const { frame, handle } = entry;
6350
+ let frameReleaseReason = "drop";
6351
+ try {
6352
+ if (!plan.runsOnboard) {
6353
+ this.noteOnboardRootIdle(deviceId, "no-source");
6354
+ return;
6355
+ }
6356
+ const pairing = onboardRootDetections(state.onboardSlot, {
6357
+ timestamp: Date.now(),
6358
+ width: frame.width,
6359
+ height: frame.height
6360
+ });
6361
+ if (pairing.kind !== "paired") {
6362
+ this.noteOnboardRootIdle(deviceId, pairing.kind);
6363
+ return;
6364
+ }
6365
+ state.onboardSlot = null;
6366
+ state.processedCount++;
6367
+ const result = {
6368
+ kind: "frame",
6369
+ frameId: `onboard-${String(deviceId)}-${String(frame.timestamp)}`,
6370
+ deviceId,
6371
+ timestamp: Date.now(),
6372
+ width: frame.width,
6373
+ height: frame.height,
6374
+ detections: pairing.detections
6375
+ };
6376
+ state.lastResultDetections = pairing.detections.length;
6377
+ await this.notifyCallbacks(deviceId, frame, result, streamType, handle, "onboard");
6378
+ frameReleaseReason = "success";
6379
+ } catch {
6380
+ frameReleaseReason = "error";
6381
+ } finally {
6382
+ try {
6383
+ if (frameReleaseReason === "success" && handle && isContinuousRetainEnabled()) await this.config.hydrateRetainedFrame?.(handle);
6384
+ } catch {}
6385
+ if (handle) this.config.releaseRetention?.(handle, frameReleaseReason);
6386
+ if (frame.frameRef) this.config.releaseFrameRef?.(frame.frameRef, frameReleaseReason);
6387
+ }
6388
+ }
6389
+ /**
6390
+ * Report — once per camera per interval — that the onboard root step decoded
6391
+ * a frame and produced nothing, with WHICH of the reasons it was. Sampled
6392
+ * because it fires at detection fps on a silent camera, which is most of the
6393
+ * time and is not itself a fault.
6394
+ */
6395
+ noteOnboardRootIdle(deviceId, reason) {
6396
+ const now = Date.now();
6397
+ const key = `${String(deviceId)}:${reason}`;
6398
+ if (now - (this.onboardIdleReportedAt.get(key) ?? 0) < 6e4) return;
6399
+ this.onboardIdleReportedAt.set(key, now);
6400
+ this.logger?.info("onboard root step produced nothing for a decoded frame", {
6401
+ tags: { deviceId },
6402
+ meta: { reason }
6403
+ });
6404
+ }
6405
+ /**
6406
+ * The camera spoke. Held until a decoded frame can be paired with it.
6407
+ *
6408
+ * Gated by the CALLER on `detectionSources` — this method is the mechanism,
6409
+ * not the policy, and a camera that does not name `onboard` never reaches it.
6410
+ */
6411
+ reportNativeDetection(deviceId, detections, frameWidth, frameHeight) {
6412
+ const state = this.cameras.get(deviceId);
6413
+ if (!state) return;
6414
+ state.onboardSlot = {
6415
+ at: Date.now(),
6416
+ detections,
6417
+ ...frameWidth !== void 0 ? { frameWidth } : {},
6418
+ ...frameHeight !== void 0 ? { frameHeight } : {}
6419
+ };
6420
+ }
6421
+ /**
6253
6422
  * Await `processFrame` with a hard bound. On expiry the permit is released
6254
6423
  * immediately (via the caller's release-once closure) and {@link TIMED_OUT}
6255
6424
  * is returned; the underlying call is left to settle into the void, since
@@ -6334,9 +6503,9 @@ var PipelineRunner = class {
6334
6503
  });
6335
6504
  }
6336
6505
  }
6337
- async notifyCallbacks(deviceId, frame, result, streamType, handle) {
6506
+ async notifyCallbacks(deviceId, frame, result, streamType, handle, source) {
6338
6507
  for (const callback of this.resultCallbacks) try {
6339
- await callback(deviceId, frame, result, streamType, handle);
6508
+ await callback(deviceId, frame, result, streamType, handle, source);
6340
6509
  } catch {}
6341
6510
  }
6342
6511
  pickNextDetectionFrame() {
@@ -7160,6 +7329,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
7160
7329
  /** Last runtime-label failure, so only a CHANGE is logged (D463). */
7161
7330
  lastRuntimeInfoFailure = null;
7162
7331
  unsubMotionEvents = null;
7332
+ unsubNativeDetections = null;
7163
7333
  /** Devices we have positively observed reporting `battery.sleeping: true`.
7164
7334
  * Absent = unknown or awake; both mean "do not gate" (D49). Fed only by
7165
7335
  * `BatteryOnStatusChanged`, so consulting it costs nothing and can never
@@ -7416,11 +7586,11 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
7416
7586
  this.runner.onDetectionStreamChange((deviceId, action) => {
7417
7587
  this.handleDetectionStreamChange(deviceId, action);
7418
7588
  });
7419
- this.runner.onResult(async (deviceId, frame, result, _streamType, handle) => {
7589
+ this.runner.onResult(async (deviceId, frame, result, _streamType, handle, source) => {
7420
7590
  this.retention?.noteInferenceResult(deviceId, handle, result);
7421
7591
  this.cutSubjectTiles(deviceId, result, handle);
7422
7592
  const carried = await this.attachBirthEvidence(deviceId, frame, result);
7423
- this.emitInferenceResult(deviceId, frame, carried, handle);
7593
+ this.emitInferenceResult(deviceId, frame, carried, handle, source);
7424
7594
  const detectionCount = result.detections?.length ?? 0;
7425
7595
  this.runner?.noteDetectionResult(deviceId, detectionCount);
7426
7596
  if (detectionCount > 0 && this.occupancyBurstInFlight.has(deviceId)) this.occupancyBurstFound.add(deviceId);
@@ -7444,13 +7614,26 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
7444
7614
  if (source === "analyzer" && cameraOwnsMotionViaSensor(attachment.config.motionSources)) return;
7445
7615
  this.runner?.reportMotion(deviceId, data.detected, source, data.regions ? [...data.regions] : void 0);
7446
7616
  });
7447
- if (this.ctx.eventBus) this.unsubBatteryEvents = this.ctx.eventBus.subscribe({ category: require_dist.EventCategory.BatteryOnStatusChanged }, (event) => {
7448
- const data = event.data;
7449
- const deviceId = data.deviceId;
7450
- if (typeof deviceId !== "number") return;
7451
- if (data.status?.sleeping === true) this.sleepingBatteryDevices.add(deviceId);
7452
- else this.sleepingBatteryDevices.delete(deviceId);
7453
- });
7617
+ if (this.ctx.eventBus) {
7618
+ this.unsubNativeDetections = this.ctx.eventBus.subscribe({ category: require_dist.EventCategory.DetectionCameraNative }, (event) => {
7619
+ const data = event.data;
7620
+ const deviceId = data.cameraId;
7621
+ if (typeof deviceId !== "number") return;
7622
+ const attachment = this.attached.get(deviceId);
7623
+ if (!attachment) return;
7624
+ if (!planDetectionSources(attachment.config.detectionSources).runsOnboard) return;
7625
+ const detections = data.detections;
7626
+ if (!detections || detections.length === 0) return;
7627
+ this.runner?.reportNativeDetection(deviceId, detections, data.frameWidth, data.frameHeight);
7628
+ });
7629
+ this.unsubBatteryEvents = this.ctx.eventBus.subscribe({ category: require_dist.EventCategory.BatteryOnStatusChanged }, (event) => {
7630
+ const data = event.data;
7631
+ const deviceId = data.deviceId;
7632
+ if (typeof deviceId !== "number") return;
7633
+ if (data.status?.sleeping === true) this.sleepingBatteryDevices.add(deviceId);
7634
+ else this.sleepingBatteryDevices.delete(deviceId);
7635
+ });
7636
+ }
7454
7637
  this.metricsSnapshotTimer = setInterval(() => this.emitMetricsSnapshot(), METRICS_SNAPSHOT_INTERVAL_MS);
7455
7638
  return {
7456
7639
  providers: [
@@ -7499,6 +7682,10 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
7499
7682
  this.unsubMotionEvents();
7500
7683
  this.unsubMotionEvents = null;
7501
7684
  }
7685
+ if (this.unsubNativeDetections) {
7686
+ this.unsubNativeDetections();
7687
+ this.unsubNativeDetections = null;
7688
+ }
7502
7689
  if (this.unsubBatteryEvents) {
7503
7690
  this.unsubBatteryEvents();
7504
7691
  this.unsubBatteryEvents = null;
@@ -10611,7 +10798,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
10611
10798
  teardown();
10612
10799
  }
10613
10800
  }
10614
- emitInferenceResult(deviceId, frame, result, handle) {
10801
+ emitInferenceResult(deviceId, frame, result, handle, source) {
10615
10802
  if (!this.ctx?.eventBus) return;
10616
10803
  const capturedAt = frame.capturedAt;
10617
10804
  const detailSteps = this.attached.get(deviceId)?.detailSteps;
@@ -10622,7 +10809,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
10622
10809
  frameHandle: handle,
10623
10810
  emittedAt: Date.now(),
10624
10811
  ...typeof capturedAt === "number" && capturedAt > 0 ? { capturedAt } : {},
10625
- ...detailSteps && detailSteps.length > 0 ? { detailSteps } : {}
10812
+ ...detailSteps && detailSteps.length > 0 ? { detailSteps } : {},
10813
+ source
10626
10814
  };
10627
10815
  this.ctx.eventBus.emit(require_dist.createEvent(require_dist.EventCategory.PipelineInferenceResult, {
10628
10816
  type: "device",
@@ -1,6 +1,6 @@
1
- import { B as customAction, G as deriveDetailCropRect, Kt as BaseAddon, S as NativeLeaseSettingsSchema, St as pipelineRunnerCapability, U as defineCustomActions, Zt as createEvent, _ as FailureCounters, b as MOTION_CLOSE_AFTER_MS, bn as EventCategory, bt as pickNativeLeaseOverride, cn as _enum, fn as lazy, hn as object, ln as array, m as DEVICE_BACKEND_TO_FORMAT, mn as number, ot as loadContributionCapability, p as DEFAULT_NATIVE_LEASE_SETTINGS, pn as literal, rn as nodePin, tt as failureContributionCapability, u as DEFAULT_DETAIL_CROP_CONVENTION, un as boolean, vn as string, x as NativeLeaseAdmissionSchema, yt as pickDetailCropConvention, zt as errMsg } from "../dist-DTPYOq7J.mjs";
2
- import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-Cz92fVGW.mjs";
3
- import { i as resolveFrameViewGeometry, l as getStepDefinition, r as localFrameRegistry } from "../default-detection-model-DC4yVaFM.mjs";
1
+ import { B as customAction, G as deriveDetailCropRect, Kt as BaseAddon, S as NativeLeaseSettingsSchema, St as pipelineRunnerCapability, U as defineCustomActions, Zt as createEvent, _ as FailureCounters, b as MOTION_CLOSE_AFTER_MS, bn as EventCategory, bt as pickNativeLeaseOverride, cn as _enum, fn as lazy, hn as object, ln as array, m as DEVICE_BACKEND_TO_FORMAT, mn as number, ot as loadContributionCapability, p as DEFAULT_NATIVE_LEASE_SETTINGS, pn as literal, rn as nodePin, tt as failureContributionCapability, u as DEFAULT_DETAIL_CROP_CONVENTION, un as boolean, vn as string, x as NativeLeaseAdmissionSchema, yt as pickDetailCropConvention, zt as errMsg } from "../dist-But4XMDJ.mjs";
2
+ import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-BHxRrsoX.mjs";
3
+ import { i as resolveFrameViewGeometry, l as getStepDefinition, r as localFrameRegistry } from "../default-detection-model-I8kuDTEi.mjs";
4
4
  import { t as isWorkerReply } from "../worker-protocol-C_W-P_g-.mjs";
5
5
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
6
6
  import { t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
@@ -2089,6 +2089,73 @@ function startSessionDecodePump(deps) {
2089
2089
  iterator.return().catch(() => {});
2090
2090
  };
2091
2091
  }
2092
+ /**
2093
+ * Read the camera's `detectionSources`.
2094
+ *
2095
+ * ABSENT is the pipeline — a runner or an attach older than the field behaves
2096
+ * exactly as it always did. EMPTY is not the same thing and must never be
2097
+ * widened into "everything": it is a camera an operator left with no root
2098
+ * detector, and inventing one for them is how a default acts without anybody
2099
+ * choosing it.
2100
+ */
2101
+ function planDetectionSources(sources) {
2102
+ if (sources === void 0) return {
2103
+ runsPipeline: true,
2104
+ runsOnboard: false
2105
+ };
2106
+ return {
2107
+ runsPipeline: sources.includes("pipeline"),
2108
+ runsOnboard: sources.includes("onboard")
2109
+ };
2110
+ }
2111
+ /** Clamp a denormalised box into the frame; a firmware box may exceed it. */
2112
+ function clamp(value, max) {
2113
+ return Math.max(0, Math.min(value, max));
2114
+ }
2115
+ /**
2116
+ * Pair the camera's last detections with a decoded frame, in the FRAME's pixel
2117
+ * space.
2118
+ *
2119
+ * The old converter denormalised against a nominal 1000×1000 when the push
2120
+ * carried no dimensions, and documented that consumers should treat a native
2121
+ * bbox as relative. That was survivable while nothing cut pixels from it. Here
2122
+ * the boxes travel with a real frame and a real handle, so they are scaled to
2123
+ * that frame — the same space every other detection on this plane lives in.
2124
+ */
2125
+ function onboardRootDetections(slot, frame) {
2126
+ if (slot === null || slot.detections.length === 0) return { kind: "none" };
2127
+ const ageMs = Math.abs(frame.timestamp - slot.at);
2128
+ if (ageMs > 1e3) return {
2129
+ kind: "expired",
2130
+ ageMs
2131
+ };
2132
+ const boxed = slot.detections.filter((d) => d.bbox != null);
2133
+ if (boxed.length === 0) return {
2134
+ kind: "no-geometry",
2135
+ classes: slot.detections.map((d) => d.class)
2136
+ };
2137
+ return {
2138
+ kind: "paired",
2139
+ detections: boxed.map((d, i) => {
2140
+ const [nx, ny, nw, nh] = d.bbox;
2141
+ const x = clamp(nx * frame.width, frame.width);
2142
+ const y = clamp(ny * frame.height, frame.height);
2143
+ return {
2144
+ id: `onboard-${String(slot.at)}-${String(i)}`,
2145
+ kind: "first-level",
2146
+ macroClass: d.class,
2147
+ score: d.confidence ?? 1,
2148
+ labels: [],
2149
+ bbox: {
2150
+ x,
2151
+ y,
2152
+ width: clamp(nw * frame.width, frame.width - x),
2153
+ height: clamp(nh * frame.height, frame.height - y)
2154
+ }
2155
+ };
2156
+ })
2157
+ };
2158
+ }
2092
2159
  //#endregion
2093
2160
  //#region src/pipeline-runner/bench-step-schema.ts
2094
2161
  /**
@@ -5636,6 +5703,8 @@ var PipelineRunner = class {
5636
5703
  cameras = /* @__PURE__ */ new Map();
5637
5704
  semaphore;
5638
5705
  resultCallbacks = [];
5706
+ /** Last report per `deviceId:reason` for {@link noteOnboardRootIdle}. */
5707
+ onboardIdleReportedAt = /* @__PURE__ */ new Map();
5639
5708
  defaultRoundRobinKeys = [];
5640
5709
  defaultRoundRobinIndex = 0;
5641
5710
  intervalHandle = null;
@@ -5716,6 +5785,7 @@ var PipelineRunner = class {
5716
5785
  const initialPhase = registration.detectionMode === "disabled" ? "idle" : registration.detectionMode === "always-on" ? "active" : "watching";
5717
5786
  const state = {
5718
5787
  registration,
5788
+ onboardSlot: null,
5719
5789
  motionQueue,
5720
5790
  detectionQueue,
5721
5791
  preRollQueue: new PreRollLane({
@@ -6192,6 +6262,15 @@ var PipelineRunner = class {
6192
6262
  const { frame, handle } = entry;
6193
6263
  state.inflight += 1;
6194
6264
  const enqueuedAt = frame._enqueuedAt ?? pickedAt;
6265
+ const plan = planDetectionSources(state.registration.detectionSources);
6266
+ if (!plan.runsPipeline) {
6267
+ try {
6268
+ await this.processOnboardRoot(deviceId, entry, state, streamType, plan);
6269
+ } finally {
6270
+ state.inflight -= 1;
6271
+ }
6272
+ return;
6273
+ }
6195
6274
  const rawRelease = await this.semaphore.acquire();
6196
6275
  let released = false;
6197
6276
  const release = () => {
@@ -6215,7 +6294,7 @@ var PipelineRunner = class {
6215
6294
  state.processedCount++;
6216
6295
  if (result) {
6217
6296
  if (result.shed === true) state.shedFrames++;
6218
- await this.notifyCallbacks(deviceId, frame, result, streamType, handle);
6297
+ await this.notifyCallbacks(deviceId, frame, result, streamType, handle, "pipeline");
6219
6298
  frameReleaseReason = "success";
6220
6299
  const emittedAt = Date.now();
6221
6300
  const capturedAt = frame.capturedAt;
@@ -6242,6 +6321,96 @@ var PipelineRunner = class {
6242
6321
  }
6243
6322
  }
6244
6323
  /**
6324
+ * The CAMERA is this camera's root step.
6325
+ *
6326
+ * Same shape as the pipeline path and deliberately so: one decoded frame, one
6327
+ * `FrameResult`, the same `notifyCallbacks`, the same handle, the same
6328
+ * retention release. Everything downstream — subject tiles, birth evidence,
6329
+ * the `detailSteps` announce, the overlay, the tracker — cannot tell which
6330
+ * detector produced the boxes, which is the entire point. What it does NOT do
6331
+ * is take an inference permit: the accelerator is never asked.
6332
+ *
6333
+ * A frame with no fresh onboard detection produces NO result rather than an
6334
+ * empty one. An empty `FrameResult` is a positive statement ("I looked and
6335
+ * there was nothing"), and this path did not look — it decoded a frame while
6336
+ * the camera happened to be silent. Emitting emptiness would tell the
6337
+ * tracker every silent frame is evidence of absence and coast every subject
6338
+ * to death between two pushes.
6339
+ */
6340
+ async processOnboardRoot(deviceId, entry, state, streamType, plan) {
6341
+ const { frame, handle } = entry;
6342
+ let frameReleaseReason = "drop";
6343
+ try {
6344
+ if (!plan.runsOnboard) {
6345
+ this.noteOnboardRootIdle(deviceId, "no-source");
6346
+ return;
6347
+ }
6348
+ const pairing = onboardRootDetections(state.onboardSlot, {
6349
+ timestamp: Date.now(),
6350
+ width: frame.width,
6351
+ height: frame.height
6352
+ });
6353
+ if (pairing.kind !== "paired") {
6354
+ this.noteOnboardRootIdle(deviceId, pairing.kind);
6355
+ return;
6356
+ }
6357
+ state.onboardSlot = null;
6358
+ state.processedCount++;
6359
+ const result = {
6360
+ kind: "frame",
6361
+ frameId: `onboard-${String(deviceId)}-${String(frame.timestamp)}`,
6362
+ deviceId,
6363
+ timestamp: Date.now(),
6364
+ width: frame.width,
6365
+ height: frame.height,
6366
+ detections: pairing.detections
6367
+ };
6368
+ state.lastResultDetections = pairing.detections.length;
6369
+ await this.notifyCallbacks(deviceId, frame, result, streamType, handle, "onboard");
6370
+ frameReleaseReason = "success";
6371
+ } catch {
6372
+ frameReleaseReason = "error";
6373
+ } finally {
6374
+ try {
6375
+ if (frameReleaseReason === "success" && handle && isContinuousRetainEnabled()) await this.config.hydrateRetainedFrame?.(handle);
6376
+ } catch {}
6377
+ if (handle) this.config.releaseRetention?.(handle, frameReleaseReason);
6378
+ if (frame.frameRef) this.config.releaseFrameRef?.(frame.frameRef, frameReleaseReason);
6379
+ }
6380
+ }
6381
+ /**
6382
+ * Report — once per camera per interval — that the onboard root step decoded
6383
+ * a frame and produced nothing, with WHICH of the reasons it was. Sampled
6384
+ * because it fires at detection fps on a silent camera, which is most of the
6385
+ * time and is not itself a fault.
6386
+ */
6387
+ noteOnboardRootIdle(deviceId, reason) {
6388
+ const now = Date.now();
6389
+ const key = `${String(deviceId)}:${reason}`;
6390
+ if (now - (this.onboardIdleReportedAt.get(key) ?? 0) < 6e4) return;
6391
+ this.onboardIdleReportedAt.set(key, now);
6392
+ this.logger?.info("onboard root step produced nothing for a decoded frame", {
6393
+ tags: { deviceId },
6394
+ meta: { reason }
6395
+ });
6396
+ }
6397
+ /**
6398
+ * The camera spoke. Held until a decoded frame can be paired with it.
6399
+ *
6400
+ * Gated by the CALLER on `detectionSources` — this method is the mechanism,
6401
+ * not the policy, and a camera that does not name `onboard` never reaches it.
6402
+ */
6403
+ reportNativeDetection(deviceId, detections, frameWidth, frameHeight) {
6404
+ const state = this.cameras.get(deviceId);
6405
+ if (!state) return;
6406
+ state.onboardSlot = {
6407
+ at: Date.now(),
6408
+ detections,
6409
+ ...frameWidth !== void 0 ? { frameWidth } : {},
6410
+ ...frameHeight !== void 0 ? { frameHeight } : {}
6411
+ };
6412
+ }
6413
+ /**
6245
6414
  * Await `processFrame` with a hard bound. On expiry the permit is released
6246
6415
  * immediately (via the caller's release-once closure) and {@link TIMED_OUT}
6247
6416
  * is returned; the underlying call is left to settle into the void, since
@@ -6326,9 +6495,9 @@ var PipelineRunner = class {
6326
6495
  });
6327
6496
  }
6328
6497
  }
6329
- async notifyCallbacks(deviceId, frame, result, streamType, handle) {
6498
+ async notifyCallbacks(deviceId, frame, result, streamType, handle, source) {
6330
6499
  for (const callback of this.resultCallbacks) try {
6331
- await callback(deviceId, frame, result, streamType, handle);
6500
+ await callback(deviceId, frame, result, streamType, handle, source);
6332
6501
  } catch {}
6333
6502
  }
6334
6503
  pickNextDetectionFrame() {
@@ -7152,6 +7321,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
7152
7321
  /** Last runtime-label failure, so only a CHANGE is logged (D463). */
7153
7322
  lastRuntimeInfoFailure = null;
7154
7323
  unsubMotionEvents = null;
7324
+ unsubNativeDetections = null;
7155
7325
  /** Devices we have positively observed reporting `battery.sleeping: true`.
7156
7326
  * Absent = unknown or awake; both mean "do not gate" (D49). Fed only by
7157
7327
  * `BatteryOnStatusChanged`, so consulting it costs nothing and can never
@@ -7408,11 +7578,11 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
7408
7578
  this.runner.onDetectionStreamChange((deviceId, action) => {
7409
7579
  this.handleDetectionStreamChange(deviceId, action);
7410
7580
  });
7411
- this.runner.onResult(async (deviceId, frame, result, _streamType, handle) => {
7581
+ this.runner.onResult(async (deviceId, frame, result, _streamType, handle, source) => {
7412
7582
  this.retention?.noteInferenceResult(deviceId, handle, result);
7413
7583
  this.cutSubjectTiles(deviceId, result, handle);
7414
7584
  const carried = await this.attachBirthEvidence(deviceId, frame, result);
7415
- this.emitInferenceResult(deviceId, frame, carried, handle);
7585
+ this.emitInferenceResult(deviceId, frame, carried, handle, source);
7416
7586
  const detectionCount = result.detections?.length ?? 0;
7417
7587
  this.runner?.noteDetectionResult(deviceId, detectionCount);
7418
7588
  if (detectionCount > 0 && this.occupancyBurstInFlight.has(deviceId)) this.occupancyBurstFound.add(deviceId);
@@ -7436,13 +7606,26 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
7436
7606
  if (source === "analyzer" && cameraOwnsMotionViaSensor(attachment.config.motionSources)) return;
7437
7607
  this.runner?.reportMotion(deviceId, data.detected, source, data.regions ? [...data.regions] : void 0);
7438
7608
  });
7439
- if (this.ctx.eventBus) this.unsubBatteryEvents = this.ctx.eventBus.subscribe({ category: EventCategory.BatteryOnStatusChanged }, (event) => {
7440
- const data = event.data;
7441
- const deviceId = data.deviceId;
7442
- if (typeof deviceId !== "number") return;
7443
- if (data.status?.sleeping === true) this.sleepingBatteryDevices.add(deviceId);
7444
- else this.sleepingBatteryDevices.delete(deviceId);
7445
- });
7609
+ if (this.ctx.eventBus) {
7610
+ this.unsubNativeDetections = this.ctx.eventBus.subscribe({ category: EventCategory.DetectionCameraNative }, (event) => {
7611
+ const data = event.data;
7612
+ const deviceId = data.cameraId;
7613
+ if (typeof deviceId !== "number") return;
7614
+ const attachment = this.attached.get(deviceId);
7615
+ if (!attachment) return;
7616
+ if (!planDetectionSources(attachment.config.detectionSources).runsOnboard) return;
7617
+ const detections = data.detections;
7618
+ if (!detections || detections.length === 0) return;
7619
+ this.runner?.reportNativeDetection(deviceId, detections, data.frameWidth, data.frameHeight);
7620
+ });
7621
+ this.unsubBatteryEvents = this.ctx.eventBus.subscribe({ category: EventCategory.BatteryOnStatusChanged }, (event) => {
7622
+ const data = event.data;
7623
+ const deviceId = data.deviceId;
7624
+ if (typeof deviceId !== "number") return;
7625
+ if (data.status?.sleeping === true) this.sleepingBatteryDevices.add(deviceId);
7626
+ else this.sleepingBatteryDevices.delete(deviceId);
7627
+ });
7628
+ }
7446
7629
  this.metricsSnapshotTimer = setInterval(() => this.emitMetricsSnapshot(), METRICS_SNAPSHOT_INTERVAL_MS);
7447
7630
  return {
7448
7631
  providers: [
@@ -7491,6 +7674,10 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
7491
7674
  this.unsubMotionEvents();
7492
7675
  this.unsubMotionEvents = null;
7493
7676
  }
7677
+ if (this.unsubNativeDetections) {
7678
+ this.unsubNativeDetections();
7679
+ this.unsubNativeDetections = null;
7680
+ }
7494
7681
  if (this.unsubBatteryEvents) {
7495
7682
  this.unsubBatteryEvents();
7496
7683
  this.unsubBatteryEvents = null;
@@ -10603,7 +10790,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
10603
10790
  teardown();
10604
10791
  }
10605
10792
  }
10606
- emitInferenceResult(deviceId, frame, result, handle) {
10793
+ emitInferenceResult(deviceId, frame, result, handle, source) {
10607
10794
  if (!this.ctx?.eventBus) return;
10608
10795
  const capturedAt = frame.capturedAt;
10609
10796
  const detailSteps = this.attached.get(deviceId)?.detailSteps;
@@ -10614,7 +10801,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
10614
10801
  frameHandle: handle,
10615
10802
  emittedAt: Date.now(),
10616
10803
  ...typeof capturedAt === "number" && capturedAt > 0 ? { capturedAt } : {},
10617
- ...detailSteps && detailSteps.length > 0 ? { detailSteps } : {}
10804
+ ...detailSteps && detailSteps.length > 0 ? { detailSteps } : {},
10805
+ source
10618
10806
  };
10619
10807
  this.ctx.eventBus.emit(createEvent(EventCategory.PipelineInferenceResult, {
10620
10808
  type: "device",
@@ -1,4 +1,4 @@
1
- import { gt as parseProcStatus } from "./dist-DTPYOq7J.mjs";
1
+ import { gt as parseProcStatus } from "./dist-But4XMDJ.mjs";
2
2
  import * as fs from "node:fs";
3
3
  import { execFile } from "node:child_process";
4
4
  //#region src/shared/node-topology-platform.ts
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("./chunk-emK7D4bc.js");
2
- const require_dist = require("./dist-p5lZW8pr.js");
2
+ const require_dist = require("./dist-NX0e_oWp.js");
3
3
  let node_fs = require("node:fs");
4
4
  node_fs = require_chunk.__toESM(node_fs);
5
5
  let node_child_process = require("node:child_process");
@@ -3,8 +3,8 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-emK7D4bc.js");
6
- const require_dist = require("../dist-p5lZW8pr.js");
7
- const require_node = require("../node-CGVUk5S9.js");
6
+ const require_dist = require("../dist-NX0e_oWp.js");
7
+ const require_node = require("../node-BgM4n8D4.js");
8
8
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
9
9
  const require_restream_intent = require("../restream-intent-C4adO0P8.js");
10
10
  const require_retire_root_keys = require("../retire-root-keys-Csb8z2f3.js");
@@ -10905,6 +10905,7 @@ Object.freeze({
10905
10905
  "motionFps": 4,
10906
10906
  "detectionFps": 10,
10907
10907
  "motionSources": ["analyzer"],
10908
+ "detectionSources": ["pipeline"],
10908
10909
  "pipelineEnabled": true,
10909
10910
  "zones": [],
10910
10911
  "onboardMotionDrivesAnalyzer": true,