@camstack/addon-post-analysis 1.1.40 → 1.1.42
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.
- package/dist/{dist-DUcGHr9E.js → dist-BveR79HO.js} +127 -32
- package/dist/{dist-CO07v2sR.mjs → dist-Cgv25jQz.mjs} +127 -32
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/{node-B7HfyyIy.js → node-C-QTkE6o.js} +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Nr0GGHJb.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DgeV9o4P.mjs} +2 -2
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CUKChUeW.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-4J_PYcat.mjs → hostInit-CnO6PKAO.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +452 -59
- package/dist/pipeline-analytics/index.mjs +451 -58
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-B4AjHkeF.mjs +0 -26
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as hydrateSchema, D as object, E as number, O as string, S as createEvent, T as boolean, _ as videoclipsCapability, a as OpsLogEntrySchema, b as BaseAddon, c as buildEventKindDescriptor, d as faceGalleryCapability, g as subKindsOf, h as plateGalleryCapability, i as MACRO_LABELS, k as EventCategory, l as cosineSimilarity, m as pipelineAnalyticsCapability, n as EVENT_KIND_BY_CAP, o as addonWidgetsSourceCapability, p as nodePin, r as EVENT_PAD_MS, s as audioMetricsCapability, t as DEFAULT_EVENT_COLOR, v as zoneAnalyticsCapability, w as array, x as DeviceType, y as errMsg } from "../dist-
|
|
1
|
+
import { C as hydrateSchema, D as object, E as number, O as string, S as createEvent, T as boolean, _ as videoclipsCapability, a as OpsLogEntrySchema, b as BaseAddon, c as buildEventKindDescriptor, d as faceGalleryCapability, g as subKindsOf, h as plateGalleryCapability, i as MACRO_LABELS, k as EventCategory, l as cosineSimilarity, m as pipelineAnalyticsCapability, n as EVENT_KIND_BY_CAP, o as addonWidgetsSourceCapability, p as nodePin, r as EVENT_PAD_MS, s as audioMetricsCapability, t as DEFAULT_EVENT_COLOR, v as zoneAnalyticsCapability, w as array, x as DeviceType, y as errMsg } from "../dist-Cgv25jQz.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import sharp from "sharp";
|
|
4
4
|
//#region src/pipeline-analytics/videoclips-provider.ts
|
|
@@ -8103,6 +8103,169 @@ function resolveTrackingSettings(raw) {
|
|
|
8103
8103
|
};
|
|
8104
8104
|
}
|
|
8105
8105
|
//#endregion
|
|
8106
|
+
//#region src/pipeline-analytics/confirmation-gate-settings.ts
|
|
8107
|
+
/**
|
|
8108
|
+
* Per-device DETECTION CONFIRMATION GATE tuning.
|
|
8109
|
+
*
|
|
8110
|
+
* Before a NEW track is born (its `start` lifecycle + firstFrame media emitted)
|
|
8111
|
+
* the gate optionally re-runs object detection on the HI-RES NATIVE CROP of the
|
|
8112
|
+
* detection box. If the crop does not confirm a compatible object above
|
|
8113
|
+
* `minConfidence`, the birth is SUPPRESSED — it was a false positive from the
|
|
8114
|
+
* low-res detection frame (a static "person" phantom, a parked-truck ghost, a
|
|
8115
|
+
* misclassified static object).
|
|
8116
|
+
*
|
|
8117
|
+
* Ships DORMANT: `enabled` defaults to `false`, so behaviour is byte-identical
|
|
8118
|
+
* to today until an operator opts in per camera. The gate is fail-OPEN — any
|
|
8119
|
+
* missing frame handle, unavailable inference cap, crop-fetch miss, re-detection
|
|
8120
|
+
* error, or timeout ALLOWS the birth (a real track is never suppressed because
|
|
8121
|
+
* confirmation was unavailable).
|
|
8122
|
+
*
|
|
8123
|
+
* Every field is independently overridable per camera; an unknown/invalid value
|
|
8124
|
+
* falls back to the field default (never throws on a bad blob) — mirrors
|
|
8125
|
+
* {@link resolveTrackingSettings}.
|
|
8126
|
+
*/
|
|
8127
|
+
/**
|
|
8128
|
+
* Per-device store keys the confirmation gate reads. FLAT (not nested under a
|
|
8129
|
+
* `confirmationGate` blob) so the fields flow through the same hydrate /
|
|
8130
|
+
* `applyDeviceSettingsPatch` path as every other per-device tunable
|
|
8131
|
+
* (trackTtlMs, minScorePerson, …) and surface directly in the operator's
|
|
8132
|
+
* Detection-pipeline → Settings form.
|
|
8133
|
+
*/
|
|
8134
|
+
var CONFIRMATION_GATE_KEYS = {
|
|
8135
|
+
enabled: "confirmationGateEnabled",
|
|
8136
|
+
minConfidence: "confirmationGateMinConfidence",
|
|
8137
|
+
minCropPx: "confirmationGateMinCropPx",
|
|
8138
|
+
timeoutMs: "confirmationGateTimeoutMs"
|
|
8139
|
+
};
|
|
8140
|
+
var ConfirmationGateSettingsSchema = object({
|
|
8141
|
+
/** Master switch. DEFAULT ON (2026-07-20 rollout) — the gate is FAIL-OPEN (any
|
|
8142
|
+
* crop-fetch / re-detection error or timeout confirms the birth), so enabling
|
|
8143
|
+
* it can only SUPPRESS false-positive births, never drop a real track on
|
|
8144
|
+
* error. Per-device store `confirmationGateEnabled=false` opts a camera out. */
|
|
8145
|
+
enabled: boolean().default(true),
|
|
8146
|
+
/**
|
|
8147
|
+
* Minimum re-detection score (0..1) required in the crop for a birth to be
|
|
8148
|
+
* confirmed. A detection must clear this AND be class-compatible with the
|
|
8149
|
+
* track's class.
|
|
8150
|
+
*/
|
|
8151
|
+
minConfidence: number().min(0).max(1).default(.4),
|
|
8152
|
+
/**
|
|
8153
|
+
* Minimum crop size (px, longest side in detection-frame pixel space) worth
|
|
8154
|
+
* re-detecting. A subject box smaller than this is too tiny to confirm
|
|
8155
|
+
* reliably, so the gate SKIPS it and fails open (confirms the birth). 0 =
|
|
8156
|
+
* confirm every birth regardless of size.
|
|
8157
|
+
*/
|
|
8158
|
+
minCropPx: number().int().min(0).default(48),
|
|
8159
|
+
/**
|
|
8160
|
+
* Per-birth confirmation budget (ms). If the crop fetch + re-detection does
|
|
8161
|
+
* not resolve within this window the gate fails open (confirms the birth) so
|
|
8162
|
+
* the synchronous frame path never stalls on inference.
|
|
8163
|
+
*/
|
|
8164
|
+
timeoutMs: number().int().min(1).default(300)
|
|
8165
|
+
});
|
|
8166
|
+
var CONFIRMATION_GATE_DEFAULTS = ConfirmationGateSettingsSchema.parse({});
|
|
8167
|
+
/**
|
|
8168
|
+
* Resolve a per-device store blob into typed confirmation-gate settings. The
|
|
8169
|
+
* gate's fields live as FLAT keys ({@link CONFIRMATION_GATE_KEYS}) in the
|
|
8170
|
+
* per-device store — the same shape the Detection-pipeline → Settings form
|
|
8171
|
+
* writes. Each field is validated independently via `.catch(default)` so an
|
|
8172
|
+
* invalid/missing field falls back to its default and parse never throws.
|
|
8173
|
+
* (A legacy nested `confirmationGate` blob, if any, is simply ignored.)
|
|
8174
|
+
*/
|
|
8175
|
+
function resolveConfirmationGateSettings(raw) {
|
|
8176
|
+
const s = ConfirmationGateSettingsSchema.shape;
|
|
8177
|
+
return {
|
|
8178
|
+
enabled: s.enabled.catch(CONFIRMATION_GATE_DEFAULTS.enabled).parse(raw[CONFIRMATION_GATE_KEYS.enabled]),
|
|
8179
|
+
minConfidence: s.minConfidence.catch(CONFIRMATION_GATE_DEFAULTS.minConfidence).parse(raw[CONFIRMATION_GATE_KEYS.minConfidence]),
|
|
8180
|
+
minCropPx: s.minCropPx.catch(CONFIRMATION_GATE_DEFAULTS.minCropPx).parse(raw[CONFIRMATION_GATE_KEYS.minCropPx]),
|
|
8181
|
+
timeoutMs: s.timeoutMs.catch(CONFIRMATION_GATE_DEFAULTS.timeoutMs).parse(raw[CONFIRMATION_GATE_KEYS.timeoutMs])
|
|
8182
|
+
};
|
|
8183
|
+
}
|
|
8184
|
+
//#endregion
|
|
8185
|
+
//#region src/pipeline-analytics/confirmation-gate.ts
|
|
8186
|
+
/**
|
|
8187
|
+
* Map a fine/raw detector or classifier class name to its coarse group. The
|
|
8188
|
+
* runtime detector already emits coarse `macroClass` values (person/vehicle/
|
|
8189
|
+
* animal), but a track's `className` may carry a finer class from a downstream
|
|
8190
|
+
* classifier — this normalizes both sides to the same three groups. Anything
|
|
8191
|
+
* unrecognized maps to `other`, which the compatibility check treats as
|
|
8192
|
+
* fail-open (compatible with everything) rather than a hard mismatch.
|
|
8193
|
+
*/
|
|
8194
|
+
function coarseGroupForClass(className) {
|
|
8195
|
+
const c = className.trim().toLowerCase();
|
|
8196
|
+
if (c === "person" || c === "people" || c === "pedestrian" || c === "human") return "person";
|
|
8197
|
+
if (c === "vehicle" || c === "car" || c === "truck" || c === "bus" || c === "van" || c === "motorcycle" || c === "motorbike" || c === "bicycle" || c === "bike") return "vehicle";
|
|
8198
|
+
if (c === "animal" || c === "dog" || c === "cat" || c === "bird" || c === "horse" || c === "sheep" || c === "cow" || c === "bear" || c === "deer" || c === "fox") return "animal";
|
|
8199
|
+
return "other";
|
|
8200
|
+
}
|
|
8201
|
+
/**
|
|
8202
|
+
* A re-detection in the crop is COMPATIBLE with the track's class when both map
|
|
8203
|
+
* to the same coarse group. When either side is unknown (`other`) we fail open
|
|
8204
|
+
* — treat it as compatible — so an unusual class never wrongly suppresses a
|
|
8205
|
+
* real track.
|
|
8206
|
+
*/
|
|
8207
|
+
function isConfirmationCompatible(trackClassName, detectionMacroClass) {
|
|
8208
|
+
const track = coarseGroupForClass(trackClassName);
|
|
8209
|
+
const det = coarseGroupForClass(detectionMacroClass);
|
|
8210
|
+
if (track === "other" || det === "other") return true;
|
|
8211
|
+
return track === det;
|
|
8212
|
+
}
|
|
8213
|
+
var failOpen = (trackId, reason) => ({
|
|
8214
|
+
trackId,
|
|
8215
|
+
confirmed: true,
|
|
8216
|
+
reason
|
|
8217
|
+
});
|
|
8218
|
+
function withTimeout(promise, timeoutMs) {
|
|
8219
|
+
return new Promise((resolve, reject) => {
|
|
8220
|
+
const timer = setTimeout(() => reject(/* @__PURE__ */ new Error("confirmation-timeout")), timeoutMs);
|
|
8221
|
+
promise.then((value) => {
|
|
8222
|
+
clearTimeout(timer);
|
|
8223
|
+
resolve(value);
|
|
8224
|
+
}, (err) => {
|
|
8225
|
+
clearTimeout(timer);
|
|
8226
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
8227
|
+
});
|
|
8228
|
+
});
|
|
8229
|
+
}
|
|
8230
|
+
async function runConfirmation(candidate, config, deps) {
|
|
8231
|
+
const crop = await deps.fetchCrop(candidate);
|
|
8232
|
+
if (!crop) return failOpen(candidate.trackId, "no-crop");
|
|
8233
|
+
const detections = await deps.redetect(crop);
|
|
8234
|
+
if (detections === null) return failOpen(candidate.trackId, "redetect-error");
|
|
8235
|
+
const confirmed = detections.some((d) => d.score >= config.minConfidence && isConfirmationCompatible(candidate.className, d.macroClass));
|
|
8236
|
+
return {
|
|
8237
|
+
trackId: candidate.trackId,
|
|
8238
|
+
confirmed,
|
|
8239
|
+
reason: confirmed ? "confirmed" : "suppressed"
|
|
8240
|
+
};
|
|
8241
|
+
}
|
|
8242
|
+
async function confirmOne(candidate, config, deps) {
|
|
8243
|
+
const cropPx = Math.max(candidate.bbox.w, candidate.bbox.h);
|
|
8244
|
+
if (config.minCropPx > 0 && cropPx < config.minCropPx) return failOpen(candidate.trackId, "below-min-crop");
|
|
8245
|
+
try {
|
|
8246
|
+
return await withTimeout(runConfirmation(candidate, config, deps), config.timeoutMs);
|
|
8247
|
+
} catch {
|
|
8248
|
+
return failOpen(candidate.trackId, "timeout");
|
|
8249
|
+
}
|
|
8250
|
+
}
|
|
8251
|
+
/**
|
|
8252
|
+
* Confirm a batch of birth candidates CONCURRENTLY and return the set of
|
|
8253
|
+
* trackIds whose births may PROCEED. When the gate is disabled (or there are no
|
|
8254
|
+
* candidates) every candidate is confirmed — byte-identical to no gate. The
|
|
8255
|
+
* caller runs this once, then processes only the confirmed births, preserving
|
|
8256
|
+
* the original birth-loop ordering.
|
|
8257
|
+
*/
|
|
8258
|
+
async function confirmBirths(candidates, config, deps) {
|
|
8259
|
+
if (!config.enabled || candidates.length === 0) return new Set(candidates.map((c) => c.trackId));
|
|
8260
|
+
const decisions = await Promise.all(candidates.map((c) => confirmOne(c, config, deps)));
|
|
8261
|
+
const confirmed = /* @__PURE__ */ new Set();
|
|
8262
|
+
for (const decision of decisions) {
|
|
8263
|
+
deps.onDecision?.(decision);
|
|
8264
|
+
if (decision.confirmed) confirmed.add(decision.trackId);
|
|
8265
|
+
}
|
|
8266
|
+
return confirmed;
|
|
8267
|
+
}
|
|
8268
|
+
//#endregion
|
|
8106
8269
|
//#region src/pipeline-analytics/face-settings.ts
|
|
8107
8270
|
/**
|
|
8108
8271
|
* Per-device face-recognition settings. Cascade: a per-device override on top
|
|
@@ -11763,6 +11926,15 @@ var DETAIL_FALLBACK_CROP_PADDING = .15;
|
|
|
11763
11926
|
* before re-reading. */
|
|
11764
11927
|
var CLIP_MODEL_ID_CACHE_TTL_MS = 6e4;
|
|
11765
11928
|
var SETTINGS_CACHE_TTL_MS = 5e3;
|
|
11929
|
+
/** TTL for the per-node confirmation-gate detector-step cache (the node's
|
|
11930
|
+
* default object-detector step rarely changes). */
|
|
11931
|
+
var CONFIRMATION_DETECTOR_CACHE_TTL_MS = 6e4;
|
|
11932
|
+
/** Padding applied to a birth box before re-detecting it — a little context
|
|
11933
|
+
* around the subject improves the confirmation detector's recall. */
|
|
11934
|
+
var CONFIRMATION_CROP_PADDING = .15;
|
|
11935
|
+
/** Cap the confirmation crop width so a 4K native surface never floods the
|
|
11936
|
+
* transport just to confirm one small box. */
|
|
11937
|
+
var CONFIRMATION_CROP_MAX_WIDTH = 320;
|
|
11766
11938
|
/** Reconcile cadence for the per-device `zones` / `zoneRules` slice handles.
|
|
11767
11939
|
* The `device.state-changed` push doesn't reliably reach a forked child, so
|
|
11768
11940
|
* each cached proxy re-pulls both slices on this timer (see ensureProxy) —
|
|
@@ -11862,6 +12034,7 @@ function toAnalyticsDeviceSections(sections) {
|
|
|
11862
12034
|
* belong in the consolidated `Detection pipeline → Settings` sub-tab alongside
|
|
11863
12035
|
* Object Detection — NOT under the generic `Analytics` top-tab:
|
|
11864
12036
|
* - `detection-sensitivity` — minHits / cooldown / stationary threshold.
|
|
12037
|
+
* - `confirmation-gate` — hi-res native-crop re-detection before a birth.
|
|
11865
12038
|
* - `tracking` — the tracker-tuning form incl. the FP knobs (dedup, person↔
|
|
11866
12039
|
* animal dedup, class voting, confirm-bypass, per-class min score).
|
|
11867
12040
|
* - `stationary-objects` — stationary promotion + occupancy tuning.
|
|
@@ -11871,6 +12044,7 @@ function toAnalyticsDeviceSections(sections) {
|
|
|
11871
12044
|
*/
|
|
11872
12045
|
var DETECTION_PIPELINE_SECTION_IDS = new Set([
|
|
11873
12046
|
"detection-sensitivity",
|
|
12047
|
+
"confirmation-gate",
|
|
11874
12048
|
"tracking",
|
|
11875
12049
|
"stationary-objects"
|
|
11876
12050
|
]);
|
|
@@ -11994,6 +12168,12 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
11994
12168
|
settingsCacheByDevice = /* @__PURE__ */ new Map();
|
|
11995
12169
|
sensitivityCacheByDevice = /* @__PURE__ */ new Map();
|
|
11996
12170
|
trackingCacheByDevice = /* @__PURE__ */ new Map();
|
|
12171
|
+
/** Per-device confirmation-gate settings cache (default off), TTL-mirrored. */
|
|
12172
|
+
confirmationGateCacheByDevice = /* @__PURE__ */ new Map();
|
|
12173
|
+
/** Per-node detector-step cache for the confirmation gate's re-detection —
|
|
12174
|
+
* the node's default root object-detector reduced to a single childless
|
|
12175
|
+
* step, resolved lazily from `pipelineExecutor.getGlobalSteps` and reused. */
|
|
12176
|
+
confirmationDetectorStepByNode = /* @__PURE__ */ new Map();
|
|
11997
12177
|
faceCacheByDevice = /* @__PURE__ */ new Map();
|
|
11998
12178
|
/** GLOBAL face-recognition master switch (addon store), TTL-cached. */
|
|
11999
12179
|
faceGlobalEnabledCache = null;
|
|
@@ -12525,6 +12705,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12525
12705
|
this.settingsCacheByDevice.delete(data.deviceId);
|
|
12526
12706
|
this.sensitivityCacheByDevice.delete(data.deviceId);
|
|
12527
12707
|
this.trackingCacheByDevice.delete(data.deviceId);
|
|
12708
|
+
this.confirmationGateCacheByDevice.delete(data.deviceId);
|
|
12528
12709
|
this.faceCacheByDevice.delete(data.deviceId);
|
|
12529
12710
|
this.mediaCacheByDevice.delete(data.deviceId);
|
|
12530
12711
|
this.packageDropCacheByDevice.delete(data.deviceId);
|
|
@@ -12542,6 +12723,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12542
12723
|
this.settingsCacheByDevice.delete(deviceId);
|
|
12543
12724
|
this.sensitivityCacheByDevice.delete(deviceId);
|
|
12544
12725
|
this.trackingCacheByDevice.delete(deviceId);
|
|
12726
|
+
this.confirmationGateCacheByDevice.delete(deviceId);
|
|
12545
12727
|
this.faceCacheByDevice.delete(deviceId);
|
|
12546
12728
|
this.mediaCacheByDevice.delete(deviceId);
|
|
12547
12729
|
this.packageDropCacheByDevice.delete(deviceId);
|
|
@@ -12893,6 +13075,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12893
13075
|
this.settingsCacheByDevice.clear();
|
|
12894
13076
|
this.sensitivityCacheByDevice.clear();
|
|
12895
13077
|
this.trackingCacheByDevice.clear();
|
|
13078
|
+
this.confirmationGateCacheByDevice.clear();
|
|
13079
|
+
this.confirmationDetectorStepByNode.clear();
|
|
12896
13080
|
this.faceCacheByDevice.clear();
|
|
12897
13081
|
this.faceGlobalEnabledCache = null;
|
|
12898
13082
|
this.mediaCacheByDevice.clear();
|
|
@@ -13008,70 +13192,88 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13008
13192
|
const prevIds = this.lastActiveTrackIds.get(key) ?? /* @__PURE__ */ new Set();
|
|
13009
13193
|
const firstFrameTargets = [];
|
|
13010
13194
|
let newTrackCount = 0;
|
|
13011
|
-
|
|
13195
|
+
const bornCandidates = [];
|
|
13196
|
+
for (const id of currentTrackIds) {
|
|
13197
|
+
if (prevIds.has(id)) continue;
|
|
13012
13198
|
const t = result.tracked.find((x) => x.trackId === id);
|
|
13013
|
-
if (t)
|
|
13014
|
-
|
|
13015
|
-
|
|
13016
|
-
|
|
13017
|
-
|
|
13018
|
-
|
|
13019
|
-
trackId: id,
|
|
13020
|
-
className: t.className,
|
|
13021
|
-
source,
|
|
13022
|
-
resurrected: true
|
|
13023
|
-
} });
|
|
13024
|
-
continue;
|
|
13025
|
-
}
|
|
13026
|
-
newTrackCount += 1;
|
|
13027
|
-
log.info("track started", { meta: {
|
|
13199
|
+
if (!t) continue;
|
|
13200
|
+
if (classifyTrackAppearance({
|
|
13201
|
+
inPrevActive: false,
|
|
13202
|
+
positionsCount: positionsCountById.get(id) ?? 1
|
|
13203
|
+
}) === "resurrection") {
|
|
13204
|
+
log.info("track resumed", { meta: {
|
|
13028
13205
|
trackId: id,
|
|
13029
13206
|
className: t.className,
|
|
13030
|
-
source
|
|
13207
|
+
source,
|
|
13208
|
+
resurrected: true
|
|
13031
13209
|
} });
|
|
13032
|
-
|
|
13033
|
-
|
|
13034
|
-
|
|
13035
|
-
|
|
13036
|
-
|
|
13037
|
-
|
|
13038
|
-
|
|
13039
|
-
|
|
13040
|
-
|
|
13041
|
-
|
|
13042
|
-
|
|
13043
|
-
|
|
13044
|
-
|
|
13045
|
-
|
|
13046
|
-
|
|
13047
|
-
|
|
13048
|
-
|
|
13049
|
-
|
|
13050
|
-
|
|
13051
|
-
|
|
13052
|
-
|
|
13053
|
-
|
|
13054
|
-
|
|
13055
|
-
|
|
13056
|
-
|
|
13057
|
-
|
|
13210
|
+
continue;
|
|
13211
|
+
}
|
|
13212
|
+
bornCandidates.push({
|
|
13213
|
+
id,
|
|
13214
|
+
t
|
|
13215
|
+
});
|
|
13216
|
+
}
|
|
13217
|
+
const confirmedBirths = await this.confirmTrackBirths(bornCandidates.map(({ id, t }) => ({
|
|
13218
|
+
trackId: id,
|
|
13219
|
+
className: t.className,
|
|
13220
|
+
bbox: t.bbox
|
|
13221
|
+
})), {
|
|
13222
|
+
deviceId,
|
|
13223
|
+
frameHandle,
|
|
13224
|
+
frameWidth: result.frameWidth,
|
|
13225
|
+
frameHeight: result.frameHeight
|
|
13226
|
+
});
|
|
13227
|
+
for (const { id, t } of bornCandidates) {
|
|
13228
|
+
if (!confirmedBirths.has(id)) continue;
|
|
13229
|
+
newTrackCount += 1;
|
|
13230
|
+
log.info("track started", { meta: {
|
|
13231
|
+
trackId: id,
|
|
13232
|
+
className: t.className,
|
|
13233
|
+
source
|
|
13234
|
+
} });
|
|
13235
|
+
if (this.eventMediaDispatcher && frameHandle) {
|
|
13236
|
+
firstFrameTargets.push({
|
|
13058
13237
|
trackId: id,
|
|
13059
|
-
|
|
13060
|
-
|
|
13061
|
-
|
|
13062
|
-
bestConfidence: t.confidence,
|
|
13063
|
-
firstSeen: result.timestamp,
|
|
13064
|
-
lastSeen: result.timestamp,
|
|
13065
|
-
...t.label !== void 0 ? { label: t.label } : {}
|
|
13066
|
-
});
|
|
13067
|
-
this.emitTrackLifecycle(startPayload, result.timestamp);
|
|
13068
|
-
this.trackLifecycleUpdateMem.set(id, {
|
|
13069
|
-
lastConfidence: t.confidence,
|
|
13070
|
-
lastEmitAt: result.timestamp,
|
|
13071
|
-
...t.label !== void 0 ? { lastLabel: t.label } : {},
|
|
13072
|
-
lastBboxArea: t.bbox.w * t.bbox.h
|
|
13238
|
+
timestamp: result.timestamp,
|
|
13239
|
+
bbox: { ...t.bbox },
|
|
13240
|
+
...t.label ? { label: t.label } : {}
|
|
13073
13241
|
});
|
|
13242
|
+
this.trackStore.seedSnapshotClock(id, result.timestamp, t.bbox);
|
|
13074
13243
|
}
|
|
13244
|
+
this.ctx.eventBus.emit({
|
|
13245
|
+
id: `pa-${randomUUID()}`,
|
|
13246
|
+
timestamp: new Date(result.timestamp),
|
|
13247
|
+
source: {
|
|
13248
|
+
type: "addon",
|
|
13249
|
+
id: "pipeline-analytics",
|
|
13250
|
+
addonId: "pipeline-analytics"
|
|
13251
|
+
},
|
|
13252
|
+
category: EventCategory.PipelineAnalyticsTrackStarted,
|
|
13253
|
+
data: {
|
|
13254
|
+
deviceId,
|
|
13255
|
+
trackId: id,
|
|
13256
|
+
className: t.className
|
|
13257
|
+
}
|
|
13258
|
+
});
|
|
13259
|
+
const startPayload = buildTrackLifecyclePayload({
|
|
13260
|
+
deviceId,
|
|
13261
|
+
trackId: id,
|
|
13262
|
+
phase: "start",
|
|
13263
|
+
classes: [t.className],
|
|
13264
|
+
bestClassName: t.className,
|
|
13265
|
+
bestConfidence: t.confidence,
|
|
13266
|
+
firstSeen: result.timestamp,
|
|
13267
|
+
lastSeen: result.timestamp,
|
|
13268
|
+
...t.label !== void 0 ? { label: t.label } : {}
|
|
13269
|
+
});
|
|
13270
|
+
this.emitTrackLifecycle(startPayload, result.timestamp);
|
|
13271
|
+
this.trackLifecycleUpdateMem.set(id, {
|
|
13272
|
+
lastConfidence: t.confidence,
|
|
13273
|
+
lastEmitAt: result.timestamp,
|
|
13274
|
+
...t.label !== void 0 ? { lastLabel: t.label } : {},
|
|
13275
|
+
lastBboxArea: t.bbox.w * t.bbox.h
|
|
13276
|
+
});
|
|
13075
13277
|
}
|
|
13076
13278
|
let lostTrackCount = 0;
|
|
13077
13279
|
for (const id of prevIds) if (!currentTrackIds.has(id)) {
|
|
@@ -13352,6 +13554,146 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13352
13554
|
});
|
|
13353
13555
|
return settings;
|
|
13354
13556
|
}
|
|
13557
|
+
/** Per-device confirmation-gate settings (default OFF), TTL-cached like the
|
|
13558
|
+
* other per-device tunables. */
|
|
13559
|
+
async resolveDeviceConfirmationGateSettings(deviceId) {
|
|
13560
|
+
const now = Date.now();
|
|
13561
|
+
const cached = this.confirmationGateCacheByDevice.get(deviceId);
|
|
13562
|
+
if (cached && now < cached.expiresAt) return cached.settings;
|
|
13563
|
+
const settings = resolveConfirmationGateSettings(await this.ctx?.settings?.readDeviceStore(deviceId) ?? {});
|
|
13564
|
+
this.confirmationGateCacheByDevice.set(deviceId, {
|
|
13565
|
+
settings,
|
|
13566
|
+
expiresAt: now + SETTINGS_CACHE_TTL_MS
|
|
13567
|
+
});
|
|
13568
|
+
return settings;
|
|
13569
|
+
}
|
|
13570
|
+
/**
|
|
13571
|
+
* Narrowed, version-skew-safe handle onto the `pipelineExecutor` cap — same
|
|
13572
|
+
* structural-cast pattern as the `pipelineRunner` handle above. A node whose
|
|
13573
|
+
* executor predates these methods simply yields `undefined`, and the
|
|
13574
|
+
* confirmation gate then fails open (allows the birth).
|
|
13575
|
+
*/
|
|
13576
|
+
pipelineExecutorApi() {
|
|
13577
|
+
const api = this.ctx?.api;
|
|
13578
|
+
if (!api) return void 0;
|
|
13579
|
+
return api.pipelineExecutor;
|
|
13580
|
+
}
|
|
13581
|
+
/**
|
|
13582
|
+
* Resolve (and cache) the confirmation gate's re-detection step for a node:
|
|
13583
|
+
* the node's DEFAULT root object-detector (from `getGlobalSteps`) reduced to a
|
|
13584
|
+
* single CHILDLESS step, so the confirmation run does detection ONLY — no
|
|
13585
|
+
* crop/classifier subtree. Cached per node (TTL) since the node default rarely
|
|
13586
|
+
* changes. `null` ⇒ no detector available ⇒ the gate fails open.
|
|
13587
|
+
*/
|
|
13588
|
+
async resolveConfirmationDetectorStep(nodeId) {
|
|
13589
|
+
const now = Date.now();
|
|
13590
|
+
const cached = this.confirmationDetectorStepByNode.get(nodeId);
|
|
13591
|
+
if (cached && now < cached.expiresAt) return cached.step;
|
|
13592
|
+
let step = null;
|
|
13593
|
+
const executor = this.pipelineExecutorApi();
|
|
13594
|
+
if (executor?.getGlobalSteps) try {
|
|
13595
|
+
const steps = await executor.getGlobalSteps.query(void 0, nodePin(nodeId));
|
|
13596
|
+
const detector = steps?.find((s) => s.slot === "detector" && s.enabled) ?? steps?.find((s) => s.slot === "detector");
|
|
13597
|
+
if (detector) step = {
|
|
13598
|
+
addonId: detector.addonId,
|
|
13599
|
+
...detector.modelId ? { modelId: detector.modelId } : {},
|
|
13600
|
+
enabled: true
|
|
13601
|
+
};
|
|
13602
|
+
} catch (err) {
|
|
13603
|
+
this.ctx.logger.debug("confirmation gate: getGlobalSteps failed", { meta: {
|
|
13604
|
+
nodeId,
|
|
13605
|
+
error: errMsg(err)
|
|
13606
|
+
} });
|
|
13607
|
+
}
|
|
13608
|
+
this.confirmationDetectorStepByNode.set(nodeId, {
|
|
13609
|
+
step,
|
|
13610
|
+
expiresAt: now + CONFIRMATION_DETECTOR_CACHE_TTL_MS
|
|
13611
|
+
});
|
|
13612
|
+
return step;
|
|
13613
|
+
}
|
|
13614
|
+
/**
|
|
13615
|
+
* Re-run object detection on a confirmation crop JPEG via the frame-owning
|
|
13616
|
+
* node's `pipelineExecutor` (pinned by `nodeId`, matching the native-crop
|
|
13617
|
+
* fetch). Returns the crop's detections, or `null` on any unavailability /
|
|
13618
|
+
* error so the gate fails open.
|
|
13619
|
+
*/
|
|
13620
|
+
async redetectCropForConfirmation(nodeId, deviceId, cropJpeg) {
|
|
13621
|
+
const step = await this.resolveConfirmationDetectorStep(nodeId);
|
|
13622
|
+
if (!step) return null;
|
|
13623
|
+
const executor = this.pipelineExecutorApi();
|
|
13624
|
+
if (!executor?.runPipeline) return null;
|
|
13625
|
+
try {
|
|
13626
|
+
const detections = (await executor.runPipeline.mutate({
|
|
13627
|
+
steps: [step],
|
|
13628
|
+
image: new Uint8Array(cropJpeg),
|
|
13629
|
+
deviceId,
|
|
13630
|
+
plane: "frame"
|
|
13631
|
+
}, nodePin(nodeId)))?.detections;
|
|
13632
|
+
if (!detections) return null;
|
|
13633
|
+
return detections.map((d) => ({
|
|
13634
|
+
macroClass: d.macroClass,
|
|
13635
|
+
score: d.score
|
|
13636
|
+
}));
|
|
13637
|
+
} catch (err) {
|
|
13638
|
+
this.ctx.logger.debug("confirmation gate: re-detection failed — fail-open", {
|
|
13639
|
+
tags: { deviceId },
|
|
13640
|
+
meta: { error: errMsg(err) }
|
|
13641
|
+
});
|
|
13642
|
+
return null;
|
|
13643
|
+
}
|
|
13644
|
+
}
|
|
13645
|
+
/**
|
|
13646
|
+
* Confirm a batch of track-birth candidates before they are created. Returns
|
|
13647
|
+
* the set of trackIds whose births may PROCEED. When the gate is disabled
|
|
13648
|
+
* (default) or no crop path is available, every candidate is confirmed —
|
|
13649
|
+
* byte-identical to no gate. Confirmations run CONCURRENTLY (one inference per
|
|
13650
|
+
* candidate) and each failure path fails OPEN, so the synchronous frame path
|
|
13651
|
+
* is never blocked or reordered by a slow/failed re-detection.
|
|
13652
|
+
*/
|
|
13653
|
+
async confirmTrackBirths(candidates, params) {
|
|
13654
|
+
const allConfirmed = () => new Set(candidates.map((c) => c.trackId));
|
|
13655
|
+
if (candidates.length === 0) return allConfirmed();
|
|
13656
|
+
const config = await this.resolveDeviceConfirmationGateSettings(params.deviceId);
|
|
13657
|
+
if (!config.enabled) return allConfirmed();
|
|
13658
|
+
const { frameHandle, frameWidth, frameHeight, deviceId } = params;
|
|
13659
|
+
const captureCrop = this.captureCrop;
|
|
13660
|
+
if (!frameHandle || !captureCrop || frameWidth <= 0 || frameHeight <= 0) {
|
|
13661
|
+
this.ctx.logger.debug("confirmation gate: no crop path — births allowed (fail-open)", {
|
|
13662
|
+
tags: { deviceId },
|
|
13663
|
+
meta: {
|
|
13664
|
+
candidates: candidates.length,
|
|
13665
|
+
hasHandle: Boolean(frameHandle)
|
|
13666
|
+
}
|
|
13667
|
+
});
|
|
13668
|
+
return allConfirmed();
|
|
13669
|
+
}
|
|
13670
|
+
const nodeId = frameHandle.nodeId;
|
|
13671
|
+
return confirmBirths(candidates, config, {
|
|
13672
|
+
fetchCrop: (candidate) => captureCrop(frameHandle, {
|
|
13673
|
+
x: candidate.bbox.x,
|
|
13674
|
+
y: candidate.bbox.y,
|
|
13675
|
+
w: candidate.bbox.w,
|
|
13676
|
+
h: candidate.bbox.h
|
|
13677
|
+
}, frameWidth, frameHeight, CONFIRMATION_CROP_PADDING, CONFIRMATION_CROP_MAX_WIDTH),
|
|
13678
|
+
redetect: (cropJpeg) => this.redetectCropForConfirmation(nodeId, deviceId, cropJpeg),
|
|
13679
|
+
onDecision: (decision) => {
|
|
13680
|
+
if (!decision.confirmed) this.ctx.logger.info("confirmation gate: birth suppressed (false positive)", {
|
|
13681
|
+
tags: { deviceId },
|
|
13682
|
+
meta: {
|
|
13683
|
+
trackId: decision.trackId,
|
|
13684
|
+
reason: decision.reason
|
|
13685
|
+
}
|
|
13686
|
+
});
|
|
13687
|
+
else if (decision.reason !== "confirmed") this.ctx.logger.debug("confirmation gate: birth allowed (fail-open)", {
|
|
13688
|
+
tags: { deviceId },
|
|
13689
|
+
meta: {
|
|
13690
|
+
trackId: decision.trackId,
|
|
13691
|
+
reason: decision.reason
|
|
13692
|
+
}
|
|
13693
|
+
});
|
|
13694
|
+
}
|
|
13695
|
+
});
|
|
13696
|
+
}
|
|
13355
13697
|
async resolveGlobalFaceEnabled() {
|
|
13356
13698
|
const now = Date.now();
|
|
13357
13699
|
if (this.faceGlobalEnabledCache && now < this.faceGlobalEnabledCache.expiresAt) return this.faceGlobalEnabledCache.value;
|
|
@@ -15625,6 +15967,56 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
15625
15967
|
}
|
|
15626
15968
|
]
|
|
15627
15969
|
},
|
|
15970
|
+
{
|
|
15971
|
+
id: "confirmation-gate",
|
|
15972
|
+
title: "Confirmation gate",
|
|
15973
|
+
description: "Before a NEW track is born, optionally re-run object detection on the hi-res native crop of the detection box. If the crop does not confirm a compatible object above the threshold, the birth is suppressed as a false positive. Fail-OPEN: any crop-fetch miss, unavailable inference, error, or timeout confirms the birth (a real track is never dropped because confirmation was unavailable).",
|
|
15974
|
+
columns: 2,
|
|
15975
|
+
fields: [
|
|
15976
|
+
{
|
|
15977
|
+
type: "boolean",
|
|
15978
|
+
key: CONFIRMATION_GATE_KEYS.enabled,
|
|
15979
|
+
label: "Confirm new tracks",
|
|
15980
|
+
description: "Re-detect the native crop before a birth. Off = every track is born from the low-res detection frame directly.",
|
|
15981
|
+
default: CONFIRMATION_GATE_DEFAULTS.enabled
|
|
15982
|
+
},
|
|
15983
|
+
{
|
|
15984
|
+
type: "slider",
|
|
15985
|
+
key: CONFIRMATION_GATE_KEYS.minConfidence,
|
|
15986
|
+
label: "Min confirmation score",
|
|
15987
|
+
description: "Minimum re-detection score (0..1) required in the crop, class-compatible with the track, for a birth to be confirmed.",
|
|
15988
|
+
min: 0,
|
|
15989
|
+
max: 1,
|
|
15990
|
+
step: .05,
|
|
15991
|
+
default: CONFIRMATION_GATE_DEFAULTS.minConfidence,
|
|
15992
|
+
showValue: true
|
|
15993
|
+
},
|
|
15994
|
+
{
|
|
15995
|
+
type: "slider",
|
|
15996
|
+
key: CONFIRMATION_GATE_KEYS.minCropPx,
|
|
15997
|
+
label: "Min crop size",
|
|
15998
|
+
description: "Subject boxes smaller than this (longest side, detection-frame px) are too tiny to confirm reliably — the gate skips them and confirms the birth. 0 = confirm every birth regardless of size.",
|
|
15999
|
+
min: 0,
|
|
16000
|
+
max: 256,
|
|
16001
|
+
step: 8,
|
|
16002
|
+
default: CONFIRMATION_GATE_DEFAULTS.minCropPx,
|
|
16003
|
+
showValue: true,
|
|
16004
|
+
unit: "px"
|
|
16005
|
+
},
|
|
16006
|
+
{
|
|
16007
|
+
type: "slider",
|
|
16008
|
+
key: CONFIRMATION_GATE_KEYS.timeoutMs,
|
|
16009
|
+
label: "Confirmation timeout",
|
|
16010
|
+
description: "Per-birth budget for crop fetch + re-detection. If it does not resolve in time the gate fails open (confirms the birth) so the frame path never stalls on inference.",
|
|
16011
|
+
min: 50,
|
|
16012
|
+
max: 2e3,
|
|
16013
|
+
step: 50,
|
|
16014
|
+
default: CONFIRMATION_GATE_DEFAULTS.timeoutMs,
|
|
16015
|
+
showValue: true,
|
|
16016
|
+
unit: "ms"
|
|
16017
|
+
}
|
|
16018
|
+
]
|
|
16019
|
+
},
|
|
15628
16020
|
{
|
|
15629
16021
|
id: "tracking",
|
|
15630
16022
|
title: "Tracking",
|
|
@@ -16064,6 +16456,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
16064
16456
|
this.settingsCacheByDevice.delete(input.deviceId);
|
|
16065
16457
|
this.sensitivityCacheByDevice.delete(input.deviceId);
|
|
16066
16458
|
this.trackingCacheByDevice.delete(input.deviceId);
|
|
16459
|
+
this.confirmationGateCacheByDevice.delete(input.deviceId);
|
|
16067
16460
|
this.forgetDeviceProcessors(input.deviceId);
|
|
16068
16461
|
return { success: true };
|
|
16069
16462
|
}
|
|
@@ -30,7 +30,7 @@ async function d(e) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
async function f() {
|
|
33
|
-
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-
|
|
33
|
+
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DgeV9o4P.mjs")).catch((e) => {
|
|
34
34
|
throw l = void 0, e;
|
|
35
35
|
}), l;
|
|
36
36
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-post-analysis",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.42",
|
|
4
4
|
"description": "CamStack Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|