@camstack/addon-post-analysis 1.1.39 → 1.1.41

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.
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-DUcGHr9E.js");
5
+ const require_dist = require("../dist-c2pR8PSR.js");
6
6
  let node_crypto = require("node:crypto");
7
7
  let sharp = require("sharp");
8
8
  sharp = require_dist.__toESM(sharp);
@@ -8108,6 +8108,152 @@ function resolveTrackingSettings(raw) {
8108
8108
  };
8109
8109
  }
8110
8110
  //#endregion
8111
+ //#region src/pipeline-analytics/confirmation-gate-settings.ts
8112
+ /**
8113
+ * Per-device DETECTION CONFIRMATION GATE tuning.
8114
+ *
8115
+ * Before a NEW track is born (its `start` lifecycle + firstFrame media emitted)
8116
+ * the gate optionally re-runs object detection on the HI-RES NATIVE CROP of the
8117
+ * detection box. If the crop does not confirm a compatible object above
8118
+ * `minConfidence`, the birth is SUPPRESSED — it was a false positive from the
8119
+ * low-res detection frame (a static "person" phantom, a parked-truck ghost, a
8120
+ * misclassified static object).
8121
+ *
8122
+ * Ships DORMANT: `enabled` defaults to `false`, so behaviour is byte-identical
8123
+ * to today until an operator opts in per camera. The gate is fail-OPEN — any
8124
+ * missing frame handle, unavailable inference cap, crop-fetch miss, re-detection
8125
+ * error, or timeout ALLOWS the birth (a real track is never suppressed because
8126
+ * confirmation was unavailable).
8127
+ *
8128
+ * Every field is independently overridable per camera; an unknown/invalid value
8129
+ * falls back to the field default (never throws on a bad blob) — mirrors
8130
+ * {@link resolveTrackingSettings}.
8131
+ */
8132
+ var ConfirmationGateSettingsSchema = require_dist.object({
8133
+ /** Master switch. OFF by default → gate never runs, births unchanged. */
8134
+ enabled: require_dist.boolean().default(false),
8135
+ /**
8136
+ * Minimum re-detection score (0..1) required in the crop for a birth to be
8137
+ * confirmed. A detection must clear this AND be class-compatible with the
8138
+ * track's class.
8139
+ */
8140
+ minConfidence: require_dist.number().min(0).max(1).default(.4),
8141
+ /**
8142
+ * Minimum crop size (px, longest side in detection-frame pixel space) worth
8143
+ * re-detecting. A subject box smaller than this is too tiny to confirm
8144
+ * reliably, so the gate SKIPS it and fails open (confirms the birth). 0 =
8145
+ * confirm every birth regardless of size.
8146
+ */
8147
+ minCropPx: require_dist.number().int().min(0).default(48),
8148
+ /**
8149
+ * Per-birth confirmation budget (ms). If the crop fetch + re-detection does
8150
+ * not resolve within this window the gate fails open (confirms the birth) so
8151
+ * the synchronous frame path never stalls on inference.
8152
+ */
8153
+ timeoutMs: require_dist.number().int().min(1).default(300)
8154
+ });
8155
+ var CONFIRMATION_GATE_DEFAULTS = ConfirmationGateSettingsSchema.parse({});
8156
+ /**
8157
+ * Resolve a per-device store blob into typed confirmation-gate settings. The
8158
+ * gate's fields live under a nested `confirmationGate` key in the per-device
8159
+ * store. Each field is validated independently via `.catch(default)` so an
8160
+ * invalid/missing field falls back to its default and parse never throws.
8161
+ */
8162
+ function resolveConfirmationGateSettings(raw) {
8163
+ const blob = require_dist.record(require_dist.string(), require_dist.unknown()).catch({}).parse(raw.confirmationGate);
8164
+ const s = ConfirmationGateSettingsSchema.shape;
8165
+ return {
8166
+ enabled: s.enabled.catch(CONFIRMATION_GATE_DEFAULTS.enabled).parse(blob.enabled),
8167
+ minConfidence: s.minConfidence.catch(CONFIRMATION_GATE_DEFAULTS.minConfidence).parse(blob.minConfidence),
8168
+ minCropPx: s.minCropPx.catch(CONFIRMATION_GATE_DEFAULTS.minCropPx).parse(blob.minCropPx),
8169
+ timeoutMs: s.timeoutMs.catch(CONFIRMATION_GATE_DEFAULTS.timeoutMs).parse(blob.timeoutMs)
8170
+ };
8171
+ }
8172
+ //#endregion
8173
+ //#region src/pipeline-analytics/confirmation-gate.ts
8174
+ /**
8175
+ * Map a fine/raw detector or classifier class name to its coarse group. The
8176
+ * runtime detector already emits coarse `macroClass` values (person/vehicle/
8177
+ * animal), but a track's `className` may carry a finer class from a downstream
8178
+ * classifier — this normalizes both sides to the same three groups. Anything
8179
+ * unrecognized maps to `other`, which the compatibility check treats as
8180
+ * fail-open (compatible with everything) rather than a hard mismatch.
8181
+ */
8182
+ function coarseGroupForClass(className) {
8183
+ const c = className.trim().toLowerCase();
8184
+ if (c === "person" || c === "people" || c === "pedestrian" || c === "human") return "person";
8185
+ if (c === "vehicle" || c === "car" || c === "truck" || c === "bus" || c === "van" || c === "motorcycle" || c === "motorbike" || c === "bicycle" || c === "bike") return "vehicle";
8186
+ if (c === "animal" || c === "dog" || c === "cat" || c === "bird" || c === "horse" || c === "sheep" || c === "cow" || c === "bear" || c === "deer" || c === "fox") return "animal";
8187
+ return "other";
8188
+ }
8189
+ /**
8190
+ * A re-detection in the crop is COMPATIBLE with the track's class when both map
8191
+ * to the same coarse group. When either side is unknown (`other`) we fail open
8192
+ * — treat it as compatible — so an unusual class never wrongly suppresses a
8193
+ * real track.
8194
+ */
8195
+ function isConfirmationCompatible(trackClassName, detectionMacroClass) {
8196
+ const track = coarseGroupForClass(trackClassName);
8197
+ const det = coarseGroupForClass(detectionMacroClass);
8198
+ if (track === "other" || det === "other") return true;
8199
+ return track === det;
8200
+ }
8201
+ var failOpen = (trackId, reason) => ({
8202
+ trackId,
8203
+ confirmed: true,
8204
+ reason
8205
+ });
8206
+ function withTimeout(promise, timeoutMs) {
8207
+ return new Promise((resolve, reject) => {
8208
+ const timer = setTimeout(() => reject(/* @__PURE__ */ new Error("confirmation-timeout")), timeoutMs);
8209
+ promise.then((value) => {
8210
+ clearTimeout(timer);
8211
+ resolve(value);
8212
+ }, (err) => {
8213
+ clearTimeout(timer);
8214
+ reject(err instanceof Error ? err : new Error(String(err)));
8215
+ });
8216
+ });
8217
+ }
8218
+ async function runConfirmation(candidate, config, deps) {
8219
+ const crop = await deps.fetchCrop(candidate);
8220
+ if (!crop) return failOpen(candidate.trackId, "no-crop");
8221
+ const detections = await deps.redetect(crop);
8222
+ if (detections === null) return failOpen(candidate.trackId, "redetect-error");
8223
+ const confirmed = detections.some((d) => d.score >= config.minConfidence && isConfirmationCompatible(candidate.className, d.macroClass));
8224
+ return {
8225
+ trackId: candidate.trackId,
8226
+ confirmed,
8227
+ reason: confirmed ? "confirmed" : "suppressed"
8228
+ };
8229
+ }
8230
+ async function confirmOne(candidate, config, deps) {
8231
+ const cropPx = Math.max(candidate.bbox.w, candidate.bbox.h);
8232
+ if (config.minCropPx > 0 && cropPx < config.minCropPx) return failOpen(candidate.trackId, "below-min-crop");
8233
+ try {
8234
+ return await withTimeout(runConfirmation(candidate, config, deps), config.timeoutMs);
8235
+ } catch {
8236
+ return failOpen(candidate.trackId, "timeout");
8237
+ }
8238
+ }
8239
+ /**
8240
+ * Confirm a batch of birth candidates CONCURRENTLY and return the set of
8241
+ * trackIds whose births may PROCEED. When the gate is disabled (or there are no
8242
+ * candidates) every candidate is confirmed — byte-identical to no gate. The
8243
+ * caller runs this once, then processes only the confirmed births, preserving
8244
+ * the original birth-loop ordering.
8245
+ */
8246
+ async function confirmBirths(candidates, config, deps) {
8247
+ if (!config.enabled || candidates.length === 0) return new Set(candidates.map((c) => c.trackId));
8248
+ const decisions = await Promise.all(candidates.map((c) => confirmOne(c, config, deps)));
8249
+ const confirmed = /* @__PURE__ */ new Set();
8250
+ for (const decision of decisions) {
8251
+ deps.onDecision?.(decision);
8252
+ if (decision.confirmed) confirmed.add(decision.trackId);
8253
+ }
8254
+ return confirmed;
8255
+ }
8256
+ //#endregion
8111
8257
  //#region src/pipeline-analytics/face-settings.ts
8112
8258
  /**
8113
8259
  * Per-device face-recognition settings. Cascade: a per-device override on top
@@ -11768,6 +11914,15 @@ var DETAIL_FALLBACK_CROP_PADDING = .15;
11768
11914
  * before re-reading. */
11769
11915
  var CLIP_MODEL_ID_CACHE_TTL_MS = 6e4;
11770
11916
  var SETTINGS_CACHE_TTL_MS = 5e3;
11917
+ /** TTL for the per-node confirmation-gate detector-step cache (the node's
11918
+ * default object-detector step rarely changes). */
11919
+ var CONFIRMATION_DETECTOR_CACHE_TTL_MS = 6e4;
11920
+ /** Padding applied to a birth box before re-detecting it — a little context
11921
+ * around the subject improves the confirmation detector's recall. */
11922
+ var CONFIRMATION_CROP_PADDING = .15;
11923
+ /** Cap the confirmation crop width so a 4K native surface never floods the
11924
+ * transport just to confirm one small box. */
11925
+ var CONFIRMATION_CROP_MAX_WIDTH = 320;
11771
11926
  /** Reconcile cadence for the per-device `zones` / `zoneRules` slice handles.
11772
11927
  * The `device.state-changed` push doesn't reliably reach a forked child, so
11773
11928
  * each cached proxy re-pulls both slices on this timer (see ensureProxy) —
@@ -11999,6 +12154,12 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
11999
12154
  settingsCacheByDevice = /* @__PURE__ */ new Map();
12000
12155
  sensitivityCacheByDevice = /* @__PURE__ */ new Map();
12001
12156
  trackingCacheByDevice = /* @__PURE__ */ new Map();
12157
+ /** Per-device confirmation-gate settings cache (default off), TTL-mirrored. */
12158
+ confirmationGateCacheByDevice = /* @__PURE__ */ new Map();
12159
+ /** Per-node detector-step cache for the confirmation gate's re-detection —
12160
+ * the node's default root object-detector reduced to a single childless
12161
+ * step, resolved lazily from `pipelineExecutor.getGlobalSteps` and reused. */
12162
+ confirmationDetectorStepByNode = /* @__PURE__ */ new Map();
12002
12163
  faceCacheByDevice = /* @__PURE__ */ new Map();
12003
12164
  /** GLOBAL face-recognition master switch (addon store), TTL-cached. */
12004
12165
  faceGlobalEnabledCache = null;
@@ -12118,7 +12279,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
12118
12279
  let storage = this.ctx.kernel.storage;
12119
12280
  const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
12120
12281
  if (mediaRoot) {
12121
- const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-B7HfyyIy.js"));
12282
+ const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-Bm-b5K4I.js"));
12122
12283
  storage = new FilesystemStorageProvider(mediaRoot);
12123
12284
  logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
12124
12285
  }
@@ -12530,6 +12691,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
12530
12691
  this.settingsCacheByDevice.delete(data.deviceId);
12531
12692
  this.sensitivityCacheByDevice.delete(data.deviceId);
12532
12693
  this.trackingCacheByDevice.delete(data.deviceId);
12694
+ this.confirmationGateCacheByDevice.delete(data.deviceId);
12533
12695
  this.faceCacheByDevice.delete(data.deviceId);
12534
12696
  this.mediaCacheByDevice.delete(data.deviceId);
12535
12697
  this.packageDropCacheByDevice.delete(data.deviceId);
@@ -12547,6 +12709,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
12547
12709
  this.settingsCacheByDevice.delete(deviceId);
12548
12710
  this.sensitivityCacheByDevice.delete(deviceId);
12549
12711
  this.trackingCacheByDevice.delete(deviceId);
12712
+ this.confirmationGateCacheByDevice.delete(deviceId);
12550
12713
  this.faceCacheByDevice.delete(deviceId);
12551
12714
  this.mediaCacheByDevice.delete(deviceId);
12552
12715
  this.packageDropCacheByDevice.delete(deviceId);
@@ -12898,6 +13061,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
12898
13061
  this.settingsCacheByDevice.clear();
12899
13062
  this.sensitivityCacheByDevice.clear();
12900
13063
  this.trackingCacheByDevice.clear();
13064
+ this.confirmationGateCacheByDevice.clear();
13065
+ this.confirmationDetectorStepByNode.clear();
12901
13066
  this.faceCacheByDevice.clear();
12902
13067
  this.faceGlobalEnabledCache = null;
12903
13068
  this.mediaCacheByDevice.clear();
@@ -13013,70 +13178,88 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
13013
13178
  const prevIds = this.lastActiveTrackIds.get(key) ?? /* @__PURE__ */ new Set();
13014
13179
  const firstFrameTargets = [];
13015
13180
  let newTrackCount = 0;
13016
- for (const id of currentTrackIds) if (!prevIds.has(id)) {
13181
+ const bornCandidates = [];
13182
+ for (const id of currentTrackIds) {
13183
+ if (prevIds.has(id)) continue;
13017
13184
  const t = result.tracked.find((x) => x.trackId === id);
13018
- if (t) {
13019
- if (classifyTrackAppearance({
13020
- inPrevActive: false,
13021
- positionsCount: positionsCountById.get(id) ?? 1
13022
- }) === "resurrection") {
13023
- log.info("track resumed", { meta: {
13024
- trackId: id,
13025
- className: t.className,
13026
- source,
13027
- resurrected: true
13028
- } });
13029
- continue;
13030
- }
13031
- newTrackCount += 1;
13032
- log.info("track started", { meta: {
13185
+ if (!t) continue;
13186
+ if (classifyTrackAppearance({
13187
+ inPrevActive: false,
13188
+ positionsCount: positionsCountById.get(id) ?? 1
13189
+ }) === "resurrection") {
13190
+ log.info("track resumed", { meta: {
13033
13191
  trackId: id,
13034
13192
  className: t.className,
13035
- source
13193
+ source,
13194
+ resurrected: true
13036
13195
  } });
13037
- if (this.eventMediaDispatcher && frameHandle) {
13038
- firstFrameTargets.push({
13039
- trackId: id,
13040
- timestamp: result.timestamp,
13041
- bbox: { ...t.bbox },
13042
- ...t.label ? { label: t.label } : {}
13043
- });
13044
- this.trackStore.seedSnapshotClock(id, result.timestamp, t.bbox);
13045
- }
13046
- this.ctx.eventBus.emit({
13047
- id: `pa-${(0, node_crypto.randomUUID)()}`,
13048
- timestamp: new Date(result.timestamp),
13049
- source: {
13050
- type: "addon",
13051
- id: "pipeline-analytics",
13052
- addonId: "pipeline-analytics"
13053
- },
13054
- category: require_dist.EventCategory.PipelineAnalyticsTrackStarted,
13055
- data: {
13056
- deviceId,
13057
- trackId: id,
13058
- className: t.className
13059
- }
13060
- });
13061
- const startPayload = buildTrackLifecyclePayload({
13062
- deviceId,
13196
+ continue;
13197
+ }
13198
+ bornCandidates.push({
13199
+ id,
13200
+ t
13201
+ });
13202
+ }
13203
+ const confirmedBirths = await this.confirmTrackBirths(bornCandidates.map(({ id, t }) => ({
13204
+ trackId: id,
13205
+ className: t.className,
13206
+ bbox: t.bbox
13207
+ })), {
13208
+ deviceId,
13209
+ frameHandle,
13210
+ frameWidth: result.frameWidth,
13211
+ frameHeight: result.frameHeight
13212
+ });
13213
+ for (const { id, t } of bornCandidates) {
13214
+ if (!confirmedBirths.has(id)) continue;
13215
+ newTrackCount += 1;
13216
+ log.info("track started", { meta: {
13217
+ trackId: id,
13218
+ className: t.className,
13219
+ source
13220
+ } });
13221
+ if (this.eventMediaDispatcher && frameHandle) {
13222
+ firstFrameTargets.push({
13063
13223
  trackId: id,
13064
- phase: "start",
13065
- classes: [t.className],
13066
- bestClassName: t.className,
13067
- bestConfidence: t.confidence,
13068
- firstSeen: result.timestamp,
13069
- lastSeen: result.timestamp,
13070
- ...t.label !== void 0 ? { label: t.label } : {}
13071
- });
13072
- this.emitTrackLifecycle(startPayload, result.timestamp);
13073
- this.trackLifecycleUpdateMem.set(id, {
13074
- lastConfidence: t.confidence,
13075
- lastEmitAt: result.timestamp,
13076
- ...t.label !== void 0 ? { lastLabel: t.label } : {},
13077
- lastBboxArea: t.bbox.w * t.bbox.h
13224
+ timestamp: result.timestamp,
13225
+ bbox: { ...t.bbox },
13226
+ ...t.label ? { label: t.label } : {}
13078
13227
  });
13228
+ this.trackStore.seedSnapshotClock(id, result.timestamp, t.bbox);
13079
13229
  }
13230
+ this.ctx.eventBus.emit({
13231
+ id: `pa-${(0, node_crypto.randomUUID)()}`,
13232
+ timestamp: new Date(result.timestamp),
13233
+ source: {
13234
+ type: "addon",
13235
+ id: "pipeline-analytics",
13236
+ addonId: "pipeline-analytics"
13237
+ },
13238
+ category: require_dist.EventCategory.PipelineAnalyticsTrackStarted,
13239
+ data: {
13240
+ deviceId,
13241
+ trackId: id,
13242
+ className: t.className
13243
+ }
13244
+ });
13245
+ const startPayload = buildTrackLifecyclePayload({
13246
+ deviceId,
13247
+ trackId: id,
13248
+ phase: "start",
13249
+ classes: [t.className],
13250
+ bestClassName: t.className,
13251
+ bestConfidence: t.confidence,
13252
+ firstSeen: result.timestamp,
13253
+ lastSeen: result.timestamp,
13254
+ ...t.label !== void 0 ? { label: t.label } : {}
13255
+ });
13256
+ this.emitTrackLifecycle(startPayload, result.timestamp);
13257
+ this.trackLifecycleUpdateMem.set(id, {
13258
+ lastConfidence: t.confidence,
13259
+ lastEmitAt: result.timestamp,
13260
+ ...t.label !== void 0 ? { lastLabel: t.label } : {},
13261
+ lastBboxArea: t.bbox.w * t.bbox.h
13262
+ });
13080
13263
  }
13081
13264
  let lostTrackCount = 0;
13082
13265
  for (const id of prevIds) if (!currentTrackIds.has(id)) {
@@ -13357,6 +13540,146 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
13357
13540
  });
13358
13541
  return settings;
13359
13542
  }
13543
+ /** Per-device confirmation-gate settings (default OFF), TTL-cached like the
13544
+ * other per-device tunables. */
13545
+ async resolveDeviceConfirmationGateSettings(deviceId) {
13546
+ const now = Date.now();
13547
+ const cached = this.confirmationGateCacheByDevice.get(deviceId);
13548
+ if (cached && now < cached.expiresAt) return cached.settings;
13549
+ const settings = resolveConfirmationGateSettings(await this.ctx?.settings?.readDeviceStore(deviceId) ?? {});
13550
+ this.confirmationGateCacheByDevice.set(deviceId, {
13551
+ settings,
13552
+ expiresAt: now + SETTINGS_CACHE_TTL_MS
13553
+ });
13554
+ return settings;
13555
+ }
13556
+ /**
13557
+ * Narrowed, version-skew-safe handle onto the `pipelineExecutor` cap — same
13558
+ * structural-cast pattern as the `pipelineRunner` handle above. A node whose
13559
+ * executor predates these methods simply yields `undefined`, and the
13560
+ * confirmation gate then fails open (allows the birth).
13561
+ */
13562
+ pipelineExecutorApi() {
13563
+ const api = this.ctx?.api;
13564
+ if (!api) return void 0;
13565
+ return api.pipelineExecutor;
13566
+ }
13567
+ /**
13568
+ * Resolve (and cache) the confirmation gate's re-detection step for a node:
13569
+ * the node's DEFAULT root object-detector (from `getGlobalSteps`) reduced to a
13570
+ * single CHILDLESS step, so the confirmation run does detection ONLY — no
13571
+ * crop/classifier subtree. Cached per node (TTL) since the node default rarely
13572
+ * changes. `null` ⇒ no detector available ⇒ the gate fails open.
13573
+ */
13574
+ async resolveConfirmationDetectorStep(nodeId) {
13575
+ const now = Date.now();
13576
+ const cached = this.confirmationDetectorStepByNode.get(nodeId);
13577
+ if (cached && now < cached.expiresAt) return cached.step;
13578
+ let step = null;
13579
+ const executor = this.pipelineExecutorApi();
13580
+ if (executor?.getGlobalSteps) try {
13581
+ const steps = await executor.getGlobalSteps.query(void 0, require_dist.nodePin(nodeId));
13582
+ const detector = steps?.find((s) => s.slot === "detector" && s.enabled) ?? steps?.find((s) => s.slot === "detector");
13583
+ if (detector) step = {
13584
+ addonId: detector.addonId,
13585
+ ...detector.modelId ? { modelId: detector.modelId } : {},
13586
+ enabled: true
13587
+ };
13588
+ } catch (err) {
13589
+ this.ctx.logger.debug("confirmation gate: getGlobalSteps failed", { meta: {
13590
+ nodeId,
13591
+ error: require_dist.errMsg(err)
13592
+ } });
13593
+ }
13594
+ this.confirmationDetectorStepByNode.set(nodeId, {
13595
+ step,
13596
+ expiresAt: now + CONFIRMATION_DETECTOR_CACHE_TTL_MS
13597
+ });
13598
+ return step;
13599
+ }
13600
+ /**
13601
+ * Re-run object detection on a confirmation crop JPEG via the frame-owning
13602
+ * node's `pipelineExecutor` (pinned by `nodeId`, matching the native-crop
13603
+ * fetch). Returns the crop's detections, or `null` on any unavailability /
13604
+ * error so the gate fails open.
13605
+ */
13606
+ async redetectCropForConfirmation(nodeId, deviceId, cropJpeg) {
13607
+ const step = await this.resolveConfirmationDetectorStep(nodeId);
13608
+ if (!step) return null;
13609
+ const executor = this.pipelineExecutorApi();
13610
+ if (!executor?.runPipeline) return null;
13611
+ try {
13612
+ const detections = (await executor.runPipeline.mutate({
13613
+ steps: [step],
13614
+ image: new Uint8Array(cropJpeg),
13615
+ deviceId,
13616
+ plane: "frame"
13617
+ }, require_dist.nodePin(nodeId)))?.detections;
13618
+ if (!detections) return null;
13619
+ return detections.map((d) => ({
13620
+ macroClass: d.macroClass,
13621
+ score: d.score
13622
+ }));
13623
+ } catch (err) {
13624
+ this.ctx.logger.debug("confirmation gate: re-detection failed — fail-open", {
13625
+ tags: { deviceId },
13626
+ meta: { error: require_dist.errMsg(err) }
13627
+ });
13628
+ return null;
13629
+ }
13630
+ }
13631
+ /**
13632
+ * Confirm a batch of track-birth candidates before they are created. Returns
13633
+ * the set of trackIds whose births may PROCEED. When the gate is disabled
13634
+ * (default) or no crop path is available, every candidate is confirmed —
13635
+ * byte-identical to no gate. Confirmations run CONCURRENTLY (one inference per
13636
+ * candidate) and each failure path fails OPEN, so the synchronous frame path
13637
+ * is never blocked or reordered by a slow/failed re-detection.
13638
+ */
13639
+ async confirmTrackBirths(candidates, params) {
13640
+ const allConfirmed = () => new Set(candidates.map((c) => c.trackId));
13641
+ if (candidates.length === 0) return allConfirmed();
13642
+ const config = await this.resolveDeviceConfirmationGateSettings(params.deviceId);
13643
+ if (!config.enabled) return allConfirmed();
13644
+ const { frameHandle, frameWidth, frameHeight, deviceId } = params;
13645
+ const captureCrop = this.captureCrop;
13646
+ if (!frameHandle || !captureCrop || frameWidth <= 0 || frameHeight <= 0) {
13647
+ this.ctx.logger.debug("confirmation gate: no crop path — births allowed (fail-open)", {
13648
+ tags: { deviceId },
13649
+ meta: {
13650
+ candidates: candidates.length,
13651
+ hasHandle: Boolean(frameHandle)
13652
+ }
13653
+ });
13654
+ return allConfirmed();
13655
+ }
13656
+ const nodeId = frameHandle.nodeId;
13657
+ return confirmBirths(candidates, config, {
13658
+ fetchCrop: (candidate) => captureCrop(frameHandle, {
13659
+ x: candidate.bbox.x,
13660
+ y: candidate.bbox.y,
13661
+ w: candidate.bbox.w,
13662
+ h: candidate.bbox.h
13663
+ }, frameWidth, frameHeight, CONFIRMATION_CROP_PADDING, CONFIRMATION_CROP_MAX_WIDTH),
13664
+ redetect: (cropJpeg) => this.redetectCropForConfirmation(nodeId, deviceId, cropJpeg),
13665
+ onDecision: (decision) => {
13666
+ if (!decision.confirmed) this.ctx.logger.info("confirmation gate: birth suppressed (false positive)", {
13667
+ tags: { deviceId },
13668
+ meta: {
13669
+ trackId: decision.trackId,
13670
+ reason: decision.reason
13671
+ }
13672
+ });
13673
+ else if (decision.reason !== "confirmed") this.ctx.logger.debug("confirmation gate: birth allowed (fail-open)", {
13674
+ tags: { deviceId },
13675
+ meta: {
13676
+ trackId: decision.trackId,
13677
+ reason: decision.reason
13678
+ }
13679
+ });
13680
+ }
13681
+ });
13682
+ }
13360
13683
  async resolveGlobalFaceEnabled() {
13361
13684
  const now = Date.now();
13362
13685
  if (this.faceGlobalEnabledCache && now < this.faceGlobalEnabledCache.expiresAt) return this.faceGlobalEnabledCache.value;
@@ -16069,6 +16392,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
16069
16392
  this.settingsCacheByDevice.delete(input.deviceId);
16070
16393
  this.sensitivityCacheByDevice.delete(input.deviceId);
16071
16394
  this.trackingCacheByDevice.delete(input.deviceId);
16395
+ this.confirmationGateCacheByDevice.delete(input.deviceId);
16072
16396
  this.forgetDeviceProcessors(input.deviceId);
16073
16397
  return { success: true };
16074
16398
  }