@camstack/addon-post-analysis 1.2.20 → 1.2.22
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/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-uZ5AHBEf.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DM0rHYe_.mjs} +3 -3
- package/dist/pipeline-analytics/{hostInit-BzSezYSo.mjs → hostInit-CJueft6_.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +271 -59
- package/dist/pipeline-analytics/index.mjs +271 -59
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ import "./dist-CYZr2fwk.mjs";
|
|
|
3
3
|
var e = {
|
|
4
4
|
"@camstack/sdk": {
|
|
5
5
|
name: "@camstack/sdk",
|
|
6
|
-
version: "1.2.
|
|
6
|
+
version: "1.2.9",
|
|
7
7
|
scope: ["default"],
|
|
8
8
|
loaded: !1,
|
|
9
9
|
from: "addon_pipeline_analytics_widgets",
|
|
@@ -18,7 +18,7 @@ var e = {
|
|
|
18
18
|
},
|
|
19
19
|
"@camstack/types": {
|
|
20
20
|
name: "@camstack/types",
|
|
21
|
-
version: "1.2.
|
|
21
|
+
version: "1.2.20",
|
|
22
22
|
scope: ["default"],
|
|
23
23
|
loaded: !1,
|
|
24
24
|
from: "addon_pipeline_analytics_widgets",
|
|
@@ -33,7 +33,7 @@ var e = {
|
|
|
33
33
|
},
|
|
34
34
|
"@camstack/ui-library": {
|
|
35
35
|
name: "@camstack/ui-library",
|
|
36
|
-
version: "1.2.
|
|
36
|
+
version: "1.2.16",
|
|
37
37
|
scope: ["default"],
|
|
38
38
|
loaded: !1,
|
|
39
39
|
from: "addon_pipeline_analytics_widgets",
|
|
@@ -36,7 +36,7 @@ async function r() {
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"@camstack/types": {
|
|
39
|
-
version: "1.2.
|
|
39
|
+
version: "1.2.20",
|
|
40
40
|
scope: "default",
|
|
41
41
|
shareConfig: {
|
|
42
42
|
singleton: !0,
|
|
@@ -45,7 +45,7 @@ async function r() {
|
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
47
|
"@camstack/sdk": {
|
|
48
|
-
version: "1.2.
|
|
48
|
+
version: "1.2.9",
|
|
49
49
|
scope: "default",
|
|
50
50
|
shareConfig: {
|
|
51
51
|
singleton: !0,
|
|
@@ -81,7 +81,7 @@ async function r() {
|
|
|
81
81
|
}
|
|
82
82
|
},
|
|
83
83
|
"@camstack/ui-library": {
|
|
84
|
-
version: "1.2.
|
|
84
|
+
version: "1.2.16",
|
|
85
85
|
scope: "default",
|
|
86
86
|
shareConfig: {
|
|
87
87
|
singleton: !0,
|
|
@@ -8268,6 +8268,80 @@ var SuppressedBirthRegistry = class {
|
|
|
8268
8268
|
}
|
|
8269
8269
|
};
|
|
8270
8270
|
//#endregion
|
|
8271
|
+
//#region src/pipeline-analytics/pipeline/deferred-births.ts
|
|
8272
|
+
var DeferredBirthRegistry = class {
|
|
8273
|
+
byDevice = /* @__PURE__ */ new Map();
|
|
8274
|
+
/** Record an undecided attempt. The first call registers; later calls count. */
|
|
8275
|
+
defer(deviceKey, trackId, nowMs) {
|
|
8276
|
+
let ids = this.byDevice.get(deviceKey);
|
|
8277
|
+
if (!ids) {
|
|
8278
|
+
ids = /* @__PURE__ */ new Map();
|
|
8279
|
+
this.byDevice.set(deviceKey, ids);
|
|
8280
|
+
}
|
|
8281
|
+
const existing = ids.get(trackId);
|
|
8282
|
+
if (existing) existing.attempts += 1;
|
|
8283
|
+
else ids.set(trackId, {
|
|
8284
|
+
firstSeenMs: nowMs,
|
|
8285
|
+
attempts: 1
|
|
8286
|
+
});
|
|
8287
|
+
}
|
|
8288
|
+
isDeferred(deviceKey, trackId) {
|
|
8289
|
+
return this.byDevice.get(deviceKey)?.has(trackId) === true;
|
|
8290
|
+
}
|
|
8291
|
+
attemptsFor(deviceKey, trackId) {
|
|
8292
|
+
return this.byDevice.get(deviceKey)?.get(trackId)?.attempts ?? 0;
|
|
8293
|
+
}
|
|
8294
|
+
/** Ms since the FIRST attempt, or 0 for an id this registry never saw. */
|
|
8295
|
+
elapsedMs(deviceKey, trackId, nowMs) {
|
|
8296
|
+
const entry = this.byDevice.get(deviceKey)?.get(trackId);
|
|
8297
|
+
return entry ? nowMs - entry.firstSeenMs : 0;
|
|
8298
|
+
}
|
|
8299
|
+
/** A verdict finally arrived — stop tracking it. */
|
|
8300
|
+
resolve(deviceKey, trackId) {
|
|
8301
|
+
const ids = this.byDevice.get(deviceKey);
|
|
8302
|
+
if (!ids) return;
|
|
8303
|
+
ids.delete(trackId);
|
|
8304
|
+
if (ids.size === 0) this.byDevice.delete(deviceKey);
|
|
8305
|
+
}
|
|
8306
|
+
/**
|
|
8307
|
+
* True when this birth has had enough tries, or waited long enough.
|
|
8308
|
+
*
|
|
8309
|
+
* Either bound ends the deferral: attempts alone would let a camera whose
|
|
8310
|
+
* frames arrive slowly hold a birth for minutes, and elapsed alone would let
|
|
8311
|
+
* a fast camera burn dozens of inference calls on one hopeless box.
|
|
8312
|
+
*/
|
|
8313
|
+
exhausted(deviceKey, trackId, nowMs, maxAttempts, maxDeferralMs) {
|
|
8314
|
+
const entry = this.byDevice.get(deviceKey)?.get(trackId);
|
|
8315
|
+
if (!entry) return false;
|
|
8316
|
+
return entry.attempts >= maxAttempts || nowMs - entry.firstSeenMs >= maxDeferralMs;
|
|
8317
|
+
}
|
|
8318
|
+
/** Every id awaiting a verdict on this device. */
|
|
8319
|
+
deferredIds(deviceKey) {
|
|
8320
|
+
const ids = this.byDevice.get(deviceKey);
|
|
8321
|
+
return ids ? [...ids.keys()] : [];
|
|
8322
|
+
}
|
|
8323
|
+
/**
|
|
8324
|
+
* Forget every deferred id the tracker is no longer carrying.
|
|
8325
|
+
*
|
|
8326
|
+
* Called once per frame with the ids present THIS frame — the same contract
|
|
8327
|
+
* as the suppressed registry's `retain`.
|
|
8328
|
+
*/
|
|
8329
|
+
retain(deviceKey, currentTrackIds) {
|
|
8330
|
+
const ids = this.byDevice.get(deviceKey);
|
|
8331
|
+
if (!ids) return;
|
|
8332
|
+
for (const id of [...ids.keys()]) if (!currentTrackIds.has(id)) ids.delete(id);
|
|
8333
|
+
if (ids.size === 0) this.byDevice.delete(deviceKey);
|
|
8334
|
+
}
|
|
8335
|
+
/** Deferred ids currently held for a device — diagnostics and tests. */
|
|
8336
|
+
size(deviceKey) {
|
|
8337
|
+
return this.byDevice.get(deviceKey)?.size ?? 0;
|
|
8338
|
+
}
|
|
8339
|
+
/** Drop a device's memory wholesale (device removed / pipeline reset). */
|
|
8340
|
+
clearDevice(deviceKey) {
|
|
8341
|
+
this.byDevice.delete(deviceKey);
|
|
8342
|
+
}
|
|
8343
|
+
};
|
|
8344
|
+
//#endregion
|
|
8271
8345
|
//#region src/pipeline-analytics/pipeline/key-event-query.ts
|
|
8272
8346
|
async function rankKeyEvents(candidates, options, peakLookup) {
|
|
8273
8347
|
const scored = [];
|
|
@@ -8796,7 +8870,7 @@ function padBbox(bbox, padding) {
|
|
|
8796
8870
|
//#endregion
|
|
8797
8871
|
//#region src/pipeline-analytics/pipeline/capture-crop.ts
|
|
8798
8872
|
function createCaptureCrop(deps) {
|
|
8799
|
-
return async (frameHandle, bbox, frameWidth, frameHeight, padding, maxWidth) => {
|
|
8873
|
+
return async (frameHandle, bbox, frameWidth, frameHeight, padding, maxWidth, deviceId) => {
|
|
8800
8874
|
const paddedNorm = padBbox({
|
|
8801
8875
|
x: bbox.x / frameWidth,
|
|
8802
8876
|
y: bbox.y / frameHeight,
|
|
@@ -8809,7 +8883,10 @@ function createCaptureCrop(deps) {
|
|
|
8809
8883
|
return nativeCrop;
|
|
8810
8884
|
}
|
|
8811
8885
|
deps.bumpCropMetric(false);
|
|
8812
|
-
deps.logger.debug("enrichment crop native miss — detail scheduler will re-run", {
|
|
8886
|
+
deps.logger.debug("enrichment crop native miss — detail scheduler will re-run", {
|
|
8887
|
+
...deviceId !== void 0 ? { tags: { deviceId } } : {},
|
|
8888
|
+
meta: { nodeId: frameHandle.nodeId }
|
|
8889
|
+
});
|
|
8813
8890
|
return null;
|
|
8814
8891
|
};
|
|
8815
8892
|
}
|
|
@@ -14540,11 +14617,15 @@ function resolveTrackingSettings(raw) {
|
|
|
14540
14617
|
* said "Ships DORMANT: `enabled` defaults to `false`" — that was stale, and on
|
|
14541
14618
|
* 2026-07-30 it nearly produced the conclusion that the gate was not running at
|
|
14542
14619
|
* all. It is: it suppressed several phantom births on device 615 that same day.
|
|
14543
|
-
* Read the schema, not this paragraph.
|
|
14544
|
-
*
|
|
14545
|
-
*
|
|
14546
|
-
*
|
|
14547
|
-
*
|
|
14620
|
+
* Read the schema, not this paragraph.
|
|
14621
|
+
*
|
|
14622
|
+
* The gate is fail-DEFERRED, not fail-open (changed 2026-08-01). Anything that
|
|
14623
|
+
* prevents a MEASUREMENT — a missing frame handle, an unavailable inference
|
|
14624
|
+
* cap, a crop-fetch miss, a re-detection error, a timeout — leaves the birth
|
|
14625
|
+
* UNDECIDED and re-tried on later frames. Only an exhausted deferral with no
|
|
14626
|
+
* crop at all falls open, and that is logged at `warn`. Fail-open on every one
|
|
14627
|
+
* of those paths is what let a brick wall onto camera 636's track feed as a
|
|
14628
|
+
* `vehicle`, and left 45% of births unmeasured over twelve hours.
|
|
14548
14629
|
*
|
|
14549
14630
|
* Every field is independently overridable per camera; an unknown/invalid value
|
|
14550
14631
|
* falls back to the field default (never throws on a bad blob) — mirrors
|
|
@@ -14561,7 +14642,9 @@ var CONFIRMATION_GATE_KEYS = {
|
|
|
14561
14642
|
enabled: "confirmationGateEnabled",
|
|
14562
14643
|
minConfidence: "confirmationGateMinConfidence",
|
|
14563
14644
|
minCropPx: "confirmationGateMinCropPx",
|
|
14564
|
-
timeoutMs: "confirmationGateTimeoutMs"
|
|
14645
|
+
timeoutMs: "confirmationGateTimeoutMs",
|
|
14646
|
+
maxDeferralMs: "confirmationGateMaxDeferralMs",
|
|
14647
|
+
maxAttempts: "confirmationGateMaxAttempts"
|
|
14565
14648
|
};
|
|
14566
14649
|
var ConfirmationGateSettingsSchema = require_dist.object({
|
|
14567
14650
|
/** Master switch. DEFAULT ON (2026-07-20 rollout) — the gate is FAIL-OPEN (any
|
|
@@ -14583,11 +14666,21 @@ var ConfirmationGateSettingsSchema = require_dist.object({
|
|
|
14583
14666
|
*/
|
|
14584
14667
|
minCropPx: require_dist.number().int().min(0).default(48),
|
|
14585
14668
|
/**
|
|
14586
|
-
* Per-
|
|
14587
|
-
* not resolve within this window the
|
|
14588
|
-
*
|
|
14669
|
+
* Per-ATTEMPT confirmation budget (ms). If the crop fetch + re-detection does
|
|
14670
|
+
* not resolve within this window the attempt ends UNDECIDED, so the
|
|
14671
|
+
* synchronous frame path never stalls on inference.
|
|
14589
14672
|
*/
|
|
14590
|
-
timeoutMs: require_dist.number().int().min(1).default(300)
|
|
14673
|
+
timeoutMs: require_dist.number().int().min(1).default(300),
|
|
14674
|
+
/**
|
|
14675
|
+
* How long a birth may stay UNDECIDED before the gate stops waiting for a
|
|
14676
|
+
* native crop and decides on the sub-native fallback.
|
|
14677
|
+
*
|
|
14678
|
+
* Measured from the FIRST attempt, so retries cannot push it out. 0 = decide
|
|
14679
|
+
* on the first attempt (the pre-2026-08-01 cadence, without the fail-open).
|
|
14680
|
+
*/
|
|
14681
|
+
maxDeferralMs: require_dist.number().int().min(0).default(2e3),
|
|
14682
|
+
/** How many gate attempts one birth may have, the first included. */
|
|
14683
|
+
maxAttempts: require_dist.number().int().min(1).default(4)
|
|
14591
14684
|
});
|
|
14592
14685
|
var CONFIRMATION_GATE_DEFAULTS = ConfirmationGateSettingsSchema.parse({});
|
|
14593
14686
|
/**
|
|
@@ -14604,7 +14697,9 @@ function resolveConfirmationGateSettings(raw) {
|
|
|
14604
14697
|
enabled: s.enabled.catch(CONFIRMATION_GATE_DEFAULTS.enabled).parse(raw[CONFIRMATION_GATE_KEYS.enabled]),
|
|
14605
14698
|
minConfidence: s.minConfidence.catch(CONFIRMATION_GATE_DEFAULTS.minConfidence).parse(raw[CONFIRMATION_GATE_KEYS.minConfidence]),
|
|
14606
14699
|
minCropPx: s.minCropPx.catch(CONFIRMATION_GATE_DEFAULTS.minCropPx).parse(raw[CONFIRMATION_GATE_KEYS.minCropPx]),
|
|
14607
|
-
timeoutMs: s.timeoutMs.catch(CONFIRMATION_GATE_DEFAULTS.timeoutMs).parse(raw[CONFIRMATION_GATE_KEYS.timeoutMs])
|
|
14700
|
+
timeoutMs: s.timeoutMs.catch(CONFIRMATION_GATE_DEFAULTS.timeoutMs).parse(raw[CONFIRMATION_GATE_KEYS.timeoutMs]),
|
|
14701
|
+
maxDeferralMs: s.maxDeferralMs.catch(CONFIRMATION_GATE_DEFAULTS.maxDeferralMs).parse(raw[CONFIRMATION_GATE_KEYS.maxDeferralMs]),
|
|
14702
|
+
maxAttempts: s.maxAttempts.catch(CONFIRMATION_GATE_DEFAULTS.maxAttempts).parse(raw[CONFIRMATION_GATE_KEYS.maxAttempts])
|
|
14608
14703
|
};
|
|
14609
14704
|
}
|
|
14610
14705
|
//#endregion
|
|
@@ -14636,9 +14731,9 @@ function isConfirmationCompatible(trackClassName, detectionMacroClass) {
|
|
|
14636
14731
|
if (track === "other" || det === "other") return true;
|
|
14637
14732
|
return track === det;
|
|
14638
14733
|
}
|
|
14639
|
-
var
|
|
14734
|
+
var undecided = (candidate, reason) => ({
|
|
14640
14735
|
trackId: candidate.trackId,
|
|
14641
|
-
|
|
14736
|
+
verdict: "undecided",
|
|
14642
14737
|
reason,
|
|
14643
14738
|
className: candidate.className
|
|
14644
14739
|
});
|
|
@@ -14654,11 +14749,12 @@ function withTimeout(promise, timeoutMs) {
|
|
|
14654
14749
|
});
|
|
14655
14750
|
});
|
|
14656
14751
|
}
|
|
14657
|
-
async function runConfirmation(candidate, config, deps) {
|
|
14658
|
-
|
|
14659
|
-
if (!crop)
|
|
14752
|
+
async function runConfirmation(candidate, config, deps, exhausted) {
|
|
14753
|
+
let crop = await deps.fetchCrop(candidate);
|
|
14754
|
+
if (!crop && exhausted && deps.fetchFallbackCrop) crop = await deps.fetchFallbackCrop(candidate);
|
|
14755
|
+
if (!crop) return undecided(candidate, "no-crop");
|
|
14660
14756
|
const detections = await deps.redetect(crop);
|
|
14661
|
-
if (detections === null) return
|
|
14757
|
+
if (detections === null) return undecided(candidate, "redetect-error");
|
|
14662
14758
|
let best;
|
|
14663
14759
|
let bestIncompatible;
|
|
14664
14760
|
for (const d of detections) if (isConfirmationCompatible(candidate.className, d.macroClass)) {
|
|
@@ -14667,7 +14763,7 @@ async function runConfirmation(candidate, config, deps) {
|
|
|
14667
14763
|
const confirmed = best !== void 0 && best.score >= config.minConfidence;
|
|
14668
14764
|
return {
|
|
14669
14765
|
trackId: candidate.trackId,
|
|
14670
|
-
confirmed,
|
|
14766
|
+
verdict: confirmed ? "confirmed" : "suppressed",
|
|
14671
14767
|
reason: confirmed ? "confirmed" : "suppressed",
|
|
14672
14768
|
className: candidate.className,
|
|
14673
14769
|
...best ? { bestScore: best.score } : {},
|
|
@@ -14677,31 +14773,40 @@ async function runConfirmation(candidate, config, deps) {
|
|
|
14677
14773
|
} : {}
|
|
14678
14774
|
};
|
|
14679
14775
|
}
|
|
14680
|
-
async function confirmOne(candidate, config, deps) {
|
|
14776
|
+
async function confirmOne(candidate, config, deps, exhausted) {
|
|
14681
14777
|
const cropPx = Math.max(candidate.bbox.w, candidate.bbox.h);
|
|
14682
|
-
if (config.minCropPx > 0 && cropPx < config.minCropPx) return
|
|
14778
|
+
if (config.minCropPx > 0 && cropPx < config.minCropPx) return undecided(candidate, "below-min-crop");
|
|
14683
14779
|
try {
|
|
14684
|
-
return await withTimeout(runConfirmation(candidate, config, deps), config.timeoutMs);
|
|
14780
|
+
return await withTimeout(runConfirmation(candidate, config, deps, exhausted), config.timeoutMs);
|
|
14685
14781
|
} catch {
|
|
14686
|
-
return
|
|
14782
|
+
return undecided(candidate, "timeout");
|
|
14687
14783
|
}
|
|
14688
14784
|
}
|
|
14689
14785
|
/**
|
|
14690
|
-
* Confirm a batch of birth candidates CONCURRENTLY
|
|
14691
|
-
*
|
|
14692
|
-
*
|
|
14693
|
-
*
|
|
14694
|
-
*
|
|
14786
|
+
* Confirm a batch of birth candidates CONCURRENTLY. When the gate is disabled
|
|
14787
|
+
* (or there are no candidates) every candidate is confirmed — byte-identical to
|
|
14788
|
+
* no gate. The caller runs this once, then processes the confirmed births,
|
|
14789
|
+
* defers the undecided ones, and retracts the rest, preserving the original
|
|
14790
|
+
* birth-loop ordering.
|
|
14695
14791
|
*/
|
|
14696
|
-
async function confirmBirths(candidates, config, deps) {
|
|
14697
|
-
if (!config.enabled || candidates.length === 0) return
|
|
14698
|
-
|
|
14792
|
+
async function confirmBirths(candidates, config, deps, exhaustedIds = /* @__PURE__ */ new Set()) {
|
|
14793
|
+
if (!config.enabled || candidates.length === 0) return {
|
|
14794
|
+
confirmed: new Set(candidates.map((c) => c.trackId)),
|
|
14795
|
+
undecided: /* @__PURE__ */ new Set()
|
|
14796
|
+
};
|
|
14797
|
+
const decisions = await Promise.all(candidates.map((c) => confirmOne(c, config, deps, exhaustedIds.has(c.trackId))));
|
|
14699
14798
|
const confirmed = /* @__PURE__ */ new Set();
|
|
14799
|
+
const pending = /* @__PURE__ */ new Set();
|
|
14700
14800
|
for (const decision of decisions) {
|
|
14701
14801
|
deps.onDecision?.(decision);
|
|
14702
|
-
if (decision.confirmed) confirmed.add(decision.trackId);
|
|
14802
|
+
if (decision.verdict === "confirmed") confirmed.add(decision.trackId);
|
|
14803
|
+
else if (decision.verdict === "undecided") if (exhaustedIds.has(decision.trackId)) confirmed.add(decision.trackId);
|
|
14804
|
+
else pending.add(decision.trackId);
|
|
14703
14805
|
}
|
|
14704
|
-
return
|
|
14806
|
+
return {
|
|
14807
|
+
confirmed,
|
|
14808
|
+
undecided: pending
|
|
14809
|
+
};
|
|
14705
14810
|
}
|
|
14706
14811
|
//#endregion
|
|
14707
14812
|
//#region src/pipeline-analytics/face-settings.ts
|
|
@@ -15320,7 +15425,7 @@ function buildDetectionSettingsSections() {
|
|
|
15320
15425
|
{
|
|
15321
15426
|
id: "confirmation-gate",
|
|
15322
15427
|
title: "Confirmation gate",
|
|
15323
|
-
description: "Before a NEW track is born,
|
|
15428
|
+
description: "Before a NEW track is born, 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-DEFERRED: a crop miss, unavailable inference, or a timeout leaves the birth UNDECIDED and looks again on later frames — it is not confirmed by default. Only an exhausted deferral with no crop at all lets a birth through unmeasured.",
|
|
15324
15429
|
columns: 2,
|
|
15325
15430
|
fields: [
|
|
15326
15431
|
{
|
|
@@ -15345,7 +15450,7 @@ function buildDetectionSettingsSections() {
|
|
|
15345
15450
|
type: "slider",
|
|
15346
15451
|
key: CONFIRMATION_GATE_KEYS.minCropPx,
|
|
15347
15452
|
label: "Min crop size",
|
|
15348
|
-
description: "Subject boxes smaller than this (longest side, detection-frame px) are too tiny to confirm reliably — the gate
|
|
15453
|
+
description: "Subject boxes smaller than this (longest side, detection-frame px) are too tiny to confirm reliably — the gate defers them and looks again as the subject approaches and the box grows. 0 = confirm every birth regardless of size.",
|
|
15349
15454
|
min: 0,
|
|
15350
15455
|
max: 256,
|
|
15351
15456
|
step: 8,
|
|
@@ -15357,13 +15462,36 @@ function buildDetectionSettingsSections() {
|
|
|
15357
15462
|
type: "slider",
|
|
15358
15463
|
key: CONFIRMATION_GATE_KEYS.timeoutMs,
|
|
15359
15464
|
label: "Confirmation timeout",
|
|
15360
|
-
description: "Per-
|
|
15465
|
+
description: "Per-ATTEMPT budget for crop fetch + re-detection. If it does not resolve in time the attempt ends undecided, so the frame path never stalls on inference.",
|
|
15361
15466
|
min: 50,
|
|
15362
15467
|
max: 2e3,
|
|
15363
15468
|
step: 50,
|
|
15364
15469
|
default: CONFIRMATION_GATE_DEFAULTS.timeoutMs,
|
|
15365
15470
|
showValue: true,
|
|
15366
15471
|
unit: "ms"
|
|
15472
|
+
},
|
|
15473
|
+
{
|
|
15474
|
+
type: "slider",
|
|
15475
|
+
key: CONFIRMATION_GATE_KEYS.maxDeferralMs,
|
|
15476
|
+
label: "Max deferral",
|
|
15477
|
+
description: "How long an undecided birth may wait for a native crop before the gate decides on the sub-native fallback. Measured from the first attempt, so retries cannot push it out. 0 = decide on the first attempt.",
|
|
15478
|
+
min: 0,
|
|
15479
|
+
max: 1e4,
|
|
15480
|
+
step: 250,
|
|
15481
|
+
default: CONFIRMATION_GATE_DEFAULTS.maxDeferralMs,
|
|
15482
|
+
showValue: true,
|
|
15483
|
+
unit: "ms"
|
|
15484
|
+
},
|
|
15485
|
+
{
|
|
15486
|
+
type: "slider",
|
|
15487
|
+
key: CONFIRMATION_GATE_KEYS.maxAttempts,
|
|
15488
|
+
label: "Max gate attempts",
|
|
15489
|
+
description: "How many times one birth may be put through the gate, the first attempt included. The deferral ends on this or on the max deferral, whichever comes first.",
|
|
15490
|
+
min: 1,
|
|
15491
|
+
max: 12,
|
|
15492
|
+
step: 1,
|
|
15493
|
+
default: CONFIRMATION_GATE_DEFAULTS.maxAttempts,
|
|
15494
|
+
showValue: true
|
|
15367
15495
|
}
|
|
15368
15496
|
]
|
|
15369
15497
|
},
|
|
@@ -20295,6 +20423,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
20295
20423
|
/** See `pipeline/suppressed-births.ts` — rejected births must not be
|
|
20296
20424
|
* re-upserted, and must be forgotten when the tracker drops the id. */
|
|
20297
20425
|
suppressedBirths = new SuppressedBirthRegistry();
|
|
20426
|
+
/** Births the gate could not MEASURE, awaiting another look on a later frame. */
|
|
20427
|
+
deferredBirths = new DeferredBirthRegistry();
|
|
20298
20428
|
lastFrameDimsByDevice = /* @__PURE__ */ new Map();
|
|
20299
20429
|
lastAudioInsertByDevice = /* @__PURE__ */ new Map();
|
|
20300
20430
|
lastMotionInsertByDevice = /* @__PURE__ */ new Map();
|
|
@@ -20380,6 +20510,15 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
20380
20510
|
* the same live-frame window as the face/plate/event-media captures. The
|
|
20381
20511
|
* optional `maxWidth` caps the native crop width (used for the full-frame key
|
|
20382
20512
|
* frame so a 4K native surface never floods the transport). */
|
|
20513
|
+
/**
|
|
20514
|
+
* The BOUNDED fallback `captureCrop` refuses — same padded ROI out of the
|
|
20515
|
+
* retained full frame (keyframe-native tier, or the runner's ≤640 RAM tier;
|
|
20516
|
+
* honest sub-native, NEVER upscaled). Built for the gallery tiles; the
|
|
20517
|
+
* confirmation gate borrows it on an EXHAUSTED deferral only. See
|
|
20518
|
+
* `docs/decisions/` — the gate is a model input, so this is a deliberate
|
|
20519
|
+
* exception to "model-input crops keep captureCrop".
|
|
20520
|
+
*/
|
|
20521
|
+
captureDisplayCropFn = null;
|
|
20383
20522
|
captureCrop = null;
|
|
20384
20523
|
/** Full NATIVE frame by handle (downscaled worker-side to `maxWidth`), routed
|
|
20385
20524
|
* to the owning runner, PLUS the source `tier`. Captured in the async init
|
|
@@ -20778,6 +20917,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
20778
20917
|
* route through the CaptureScheduler at the injection point (S4). */
|
|
20779
20918
|
buildRecognizers(api, logger, stores, transport) {
|
|
20780
20919
|
const captureDisplayCrop = transport.captureDisplayCrop;
|
|
20920
|
+
this.captureDisplayCropFn = captureDisplayCrop;
|
|
20781
20921
|
this.faceRecognizer = new FaceRecognizer({
|
|
20782
20922
|
identityStore: stores.identityStore,
|
|
20783
20923
|
faceStore: stores.faceStore,
|
|
@@ -21436,7 +21576,19 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
21436
21576
|
t
|
|
21437
21577
|
});
|
|
21438
21578
|
}
|
|
21439
|
-
const
|
|
21579
|
+
for (const id of this.deferredBirths.deferredIds(key)) {
|
|
21580
|
+
if (bornCandidates.some((c) => c.id === id)) continue;
|
|
21581
|
+
const t = result.tracked.find((x) => x.trackId === id);
|
|
21582
|
+
if (t) bornCandidates.push({
|
|
21583
|
+
id,
|
|
21584
|
+
t
|
|
21585
|
+
});
|
|
21586
|
+
}
|
|
21587
|
+
const gateSettings = await this.resolveDeviceConfirmationGateSettings(deviceId);
|
|
21588
|
+
const exhaustionNowMs = Date.now();
|
|
21589
|
+
const exhaustedIds = /* @__PURE__ */ new Set();
|
|
21590
|
+
for (const { id } of bornCandidates) if (this.deferredBirths.exhausted(key, id, exhaustionNowMs, gateSettings.maxAttempts, gateSettings.maxDeferralMs)) exhaustedIds.add(id);
|
|
21591
|
+
const outcome = await this.confirmTrackBirths(bornCandidates.map(({ id, t }) => ({
|
|
21440
21592
|
trackId: id,
|
|
21441
21593
|
className: t.className,
|
|
21442
21594
|
bbox: t.bbox
|
|
@@ -21445,18 +21597,53 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
21445
21597
|
frameHandle,
|
|
21446
21598
|
frameWidth: result.frameWidth,
|
|
21447
21599
|
frameHeight: result.frameHeight
|
|
21448
|
-
});
|
|
21600
|
+
}, exhaustedIds);
|
|
21601
|
+
const gateNowMs = Date.now();
|
|
21449
21602
|
for (const { id, t } of bornCandidates) {
|
|
21450
|
-
if (
|
|
21603
|
+
if (outcome.undecided.has(id)) {
|
|
21604
|
+
this.deferredBirths.defer(key, id, gateNowMs);
|
|
21605
|
+
log.info("birth undecided — deferred for another look", { meta: {
|
|
21606
|
+
trackId: id,
|
|
21607
|
+
className: t.className,
|
|
21608
|
+
attempts: this.deferredBirths.attemptsFor(key, id),
|
|
21609
|
+
elapsedMs: this.deferredBirths.elapsedMs(key, id, gateNowMs),
|
|
21610
|
+
source
|
|
21611
|
+
} });
|
|
21612
|
+
continue;
|
|
21613
|
+
}
|
|
21614
|
+
const wasDeferred = this.deferredBirths.isDeferred(key, id);
|
|
21615
|
+
const deferredForMs = this.deferredBirths.elapsedMs(key, id, gateNowMs);
|
|
21616
|
+
const deferredAttempts = this.deferredBirths.attemptsFor(key, id);
|
|
21617
|
+
this.deferredBirths.resolve(key, id);
|
|
21618
|
+
if (!outcome.confirmed.has(id)) {
|
|
21451
21619
|
this.suppressedBirths.reject(key, id);
|
|
21452
21620
|
this.trackStore?.dropActive(id);
|
|
21453
21621
|
log.info("birth suppressed — track record retracted", { meta: {
|
|
21454
21622
|
trackId: id,
|
|
21455
21623
|
className: t.className,
|
|
21456
|
-
source
|
|
21624
|
+
source,
|
|
21625
|
+
...wasDeferred ? {
|
|
21626
|
+
wasDeferred,
|
|
21627
|
+
deferredForMs,
|
|
21628
|
+
deferredAttempts
|
|
21629
|
+
} : {}
|
|
21457
21630
|
} });
|
|
21458
21631
|
continue;
|
|
21459
21632
|
}
|
|
21633
|
+
if (wasDeferred && exhaustedIds.has(id)) log.warn("birth allowed unmeasured — deferral exhausted, no crop available", { meta: {
|
|
21634
|
+
trackId: id,
|
|
21635
|
+
className: t.className,
|
|
21636
|
+
source,
|
|
21637
|
+
deferredForMs,
|
|
21638
|
+
deferredAttempts
|
|
21639
|
+
} });
|
|
21640
|
+
else if (wasDeferred) log.info("birth decided late — confirmed after deferral", { meta: {
|
|
21641
|
+
trackId: id,
|
|
21642
|
+
className: t.className,
|
|
21643
|
+
source,
|
|
21644
|
+
deferredForMs,
|
|
21645
|
+
deferredAttempts
|
|
21646
|
+
} });
|
|
21460
21647
|
newTrackCount += 1;
|
|
21461
21648
|
log.info("track started", { meta: {
|
|
21462
21649
|
trackId: id,
|
|
@@ -21518,6 +21705,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
21518
21705
|
} });
|
|
21519
21706
|
}
|
|
21520
21707
|
this.suppressedBirths.retain(key, currentTrackIds);
|
|
21708
|
+
this.deferredBirths.retain(key, currentTrackIds);
|
|
21521
21709
|
this.lastActiveTrackIds.set(key, currentTrackIds);
|
|
21522
21710
|
const stationarySettings = this.stationarySettingsFromCache(deviceId);
|
|
21523
21711
|
if (source === "pipeline" && this.stationaryRegistry && stationarySettings.enabled) {
|
|
@@ -21946,19 +22134,25 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
21946
22134
|
* candidate) and each failure path fails OPEN, so the synchronous frame path
|
|
21947
22135
|
* is never blocked or reordered by a slow/failed re-detection.
|
|
21948
22136
|
*/
|
|
21949
|
-
async confirmTrackBirths(candidates, params) {
|
|
21950
|
-
const allConfirmed = () =>
|
|
22137
|
+
async confirmTrackBirths(candidates, params, exhaustedIds = /* @__PURE__ */ new Set()) {
|
|
22138
|
+
const allConfirmed = () => ({
|
|
22139
|
+
confirmed: new Set(candidates.map((c) => c.trackId)),
|
|
22140
|
+
undecided: /* @__PURE__ */ new Set()
|
|
22141
|
+
});
|
|
21951
22142
|
if (candidates.length === 0) return allConfirmed();
|
|
21952
22143
|
const config = await this.resolveDeviceConfirmationGateSettings(params.deviceId);
|
|
21953
22144
|
if (!config.enabled) return allConfirmed();
|
|
21954
22145
|
const { frameHandle, frameWidth, frameHeight, deviceId } = params;
|
|
21955
22146
|
const captureCrop = this.captureCrop;
|
|
21956
22147
|
if (!frameHandle || !captureCrop || frameWidth <= 0 || frameHeight <= 0) {
|
|
21957
|
-
this.ctx.logger.
|
|
22148
|
+
this.ctx.logger.info("confirmation gate: no crop path — births allowed unmeasured", {
|
|
21958
22149
|
tags: { deviceId },
|
|
21959
22150
|
meta: {
|
|
21960
22151
|
candidates: candidates.length,
|
|
21961
|
-
hasHandle: Boolean(frameHandle)
|
|
22152
|
+
hasHandle: Boolean(frameHandle),
|
|
22153
|
+
hasCaptureCrop: Boolean(captureCrop),
|
|
22154
|
+
frameWidth,
|
|
22155
|
+
frameHeight
|
|
21962
22156
|
}
|
|
21963
22157
|
});
|
|
21964
22158
|
return allConfirmed();
|
|
@@ -21974,8 +22168,18 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
21974
22168
|
y: candidate.bbox.y,
|
|
21975
22169
|
w: candidate.bbox.w,
|
|
21976
22170
|
h: candidate.bbox.h
|
|
21977
|
-
}, frameWidth, frameHeight, CONFIRMATION_CROP_PADDING, CONFIRMATION_CROP_MAX_WIDTH)
|
|
22171
|
+
}, frameWidth, frameHeight, CONFIRMATION_CROP_PADDING, CONFIRMATION_CROP_MAX_WIDTH, deviceId)
|
|
21978
22172
|
}),
|
|
22173
|
+
fetchFallbackCrop: (candidate) => {
|
|
22174
|
+
const displayCrop = this.captureDisplayCropFn;
|
|
22175
|
+
if (!displayCrop) return Promise.resolve(null);
|
|
22176
|
+
return displayCrop(frameHandle, {
|
|
22177
|
+
x: candidate.bbox.x,
|
|
22178
|
+
y: candidate.bbox.y,
|
|
22179
|
+
w: candidate.bbox.w,
|
|
22180
|
+
h: candidate.bbox.h
|
|
22181
|
+
}, frameWidth, frameHeight, CONFIRMATION_CROP_PADDING, candidate.trackId, CONFIRMATION_CROP_MAX_WIDTH);
|
|
22182
|
+
},
|
|
21979
22183
|
redetect: (cropJpeg) => this.redetectCropForConfirmation(nodeId, deviceId, cropJpeg),
|
|
21980
22184
|
onDecision: (decision) => {
|
|
21981
22185
|
const meta = {
|
|
@@ -21989,20 +22193,28 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
21989
22193
|
} : {},
|
|
21990
22194
|
minConfidence: config.minConfidence
|
|
21991
22195
|
};
|
|
21992
|
-
|
|
21993
|
-
|
|
21994
|
-
|
|
21995
|
-
|
|
21996
|
-
|
|
21997
|
-
|
|
21998
|
-
|
|
21999
|
-
|
|
22000
|
-
|
|
22001
|
-
|
|
22002
|
-
|
|
22003
|
-
|
|
22196
|
+
switch (decision.verdict) {
|
|
22197
|
+
case "suppressed":
|
|
22198
|
+
this.ctx.logger.info("confirmation gate: birth suppressed (false positive)", {
|
|
22199
|
+
tags: { deviceId },
|
|
22200
|
+
meta
|
|
22201
|
+
});
|
|
22202
|
+
break;
|
|
22203
|
+
case "undecided":
|
|
22204
|
+
this.ctx.logger.info("confirmation gate: birth undecided", {
|
|
22205
|
+
tags: { deviceId },
|
|
22206
|
+
meta
|
|
22207
|
+
});
|
|
22208
|
+
break;
|
|
22209
|
+
case "confirmed":
|
|
22210
|
+
this.ctx.logger.info("confirmation gate: birth confirmed", {
|
|
22211
|
+
tags: { deviceId },
|
|
22212
|
+
meta
|
|
22213
|
+
});
|
|
22214
|
+
break;
|
|
22215
|
+
}
|
|
22004
22216
|
}
|
|
22005
|
-
});
|
|
22217
|
+
}, exhaustedIds);
|
|
22006
22218
|
}
|
|
22007
22219
|
async resolveGlobalFaceEnabled() {
|
|
22008
22220
|
return this.faceGlobalEnabledCache.get(() => this.faceGlobalEnabledState.get());
|
|
@@ -8262,6 +8262,80 @@ var SuppressedBirthRegistry = class {
|
|
|
8262
8262
|
}
|
|
8263
8263
|
};
|
|
8264
8264
|
//#endregion
|
|
8265
|
+
//#region src/pipeline-analytics/pipeline/deferred-births.ts
|
|
8266
|
+
var DeferredBirthRegistry = class {
|
|
8267
|
+
byDevice = /* @__PURE__ */ new Map();
|
|
8268
|
+
/** Record an undecided attempt. The first call registers; later calls count. */
|
|
8269
|
+
defer(deviceKey, trackId, nowMs) {
|
|
8270
|
+
let ids = this.byDevice.get(deviceKey);
|
|
8271
|
+
if (!ids) {
|
|
8272
|
+
ids = /* @__PURE__ */ new Map();
|
|
8273
|
+
this.byDevice.set(deviceKey, ids);
|
|
8274
|
+
}
|
|
8275
|
+
const existing = ids.get(trackId);
|
|
8276
|
+
if (existing) existing.attempts += 1;
|
|
8277
|
+
else ids.set(trackId, {
|
|
8278
|
+
firstSeenMs: nowMs,
|
|
8279
|
+
attempts: 1
|
|
8280
|
+
});
|
|
8281
|
+
}
|
|
8282
|
+
isDeferred(deviceKey, trackId) {
|
|
8283
|
+
return this.byDevice.get(deviceKey)?.has(trackId) === true;
|
|
8284
|
+
}
|
|
8285
|
+
attemptsFor(deviceKey, trackId) {
|
|
8286
|
+
return this.byDevice.get(deviceKey)?.get(trackId)?.attempts ?? 0;
|
|
8287
|
+
}
|
|
8288
|
+
/** Ms since the FIRST attempt, or 0 for an id this registry never saw. */
|
|
8289
|
+
elapsedMs(deviceKey, trackId, nowMs) {
|
|
8290
|
+
const entry = this.byDevice.get(deviceKey)?.get(trackId);
|
|
8291
|
+
return entry ? nowMs - entry.firstSeenMs : 0;
|
|
8292
|
+
}
|
|
8293
|
+
/** A verdict finally arrived — stop tracking it. */
|
|
8294
|
+
resolve(deviceKey, trackId) {
|
|
8295
|
+
const ids = this.byDevice.get(deviceKey);
|
|
8296
|
+
if (!ids) return;
|
|
8297
|
+
ids.delete(trackId);
|
|
8298
|
+
if (ids.size === 0) this.byDevice.delete(deviceKey);
|
|
8299
|
+
}
|
|
8300
|
+
/**
|
|
8301
|
+
* True when this birth has had enough tries, or waited long enough.
|
|
8302
|
+
*
|
|
8303
|
+
* Either bound ends the deferral: attempts alone would let a camera whose
|
|
8304
|
+
* frames arrive slowly hold a birth for minutes, and elapsed alone would let
|
|
8305
|
+
* a fast camera burn dozens of inference calls on one hopeless box.
|
|
8306
|
+
*/
|
|
8307
|
+
exhausted(deviceKey, trackId, nowMs, maxAttempts, maxDeferralMs) {
|
|
8308
|
+
const entry = this.byDevice.get(deviceKey)?.get(trackId);
|
|
8309
|
+
if (!entry) return false;
|
|
8310
|
+
return entry.attempts >= maxAttempts || nowMs - entry.firstSeenMs >= maxDeferralMs;
|
|
8311
|
+
}
|
|
8312
|
+
/** Every id awaiting a verdict on this device. */
|
|
8313
|
+
deferredIds(deviceKey) {
|
|
8314
|
+
const ids = this.byDevice.get(deviceKey);
|
|
8315
|
+
return ids ? [...ids.keys()] : [];
|
|
8316
|
+
}
|
|
8317
|
+
/**
|
|
8318
|
+
* Forget every deferred id the tracker is no longer carrying.
|
|
8319
|
+
*
|
|
8320
|
+
* Called once per frame with the ids present THIS frame — the same contract
|
|
8321
|
+
* as the suppressed registry's `retain`.
|
|
8322
|
+
*/
|
|
8323
|
+
retain(deviceKey, currentTrackIds) {
|
|
8324
|
+
const ids = this.byDevice.get(deviceKey);
|
|
8325
|
+
if (!ids) return;
|
|
8326
|
+
for (const id of [...ids.keys()]) if (!currentTrackIds.has(id)) ids.delete(id);
|
|
8327
|
+
if (ids.size === 0) this.byDevice.delete(deviceKey);
|
|
8328
|
+
}
|
|
8329
|
+
/** Deferred ids currently held for a device — diagnostics and tests. */
|
|
8330
|
+
size(deviceKey) {
|
|
8331
|
+
return this.byDevice.get(deviceKey)?.size ?? 0;
|
|
8332
|
+
}
|
|
8333
|
+
/** Drop a device's memory wholesale (device removed / pipeline reset). */
|
|
8334
|
+
clearDevice(deviceKey) {
|
|
8335
|
+
this.byDevice.delete(deviceKey);
|
|
8336
|
+
}
|
|
8337
|
+
};
|
|
8338
|
+
//#endregion
|
|
8265
8339
|
//#region src/pipeline-analytics/pipeline/key-event-query.ts
|
|
8266
8340
|
async function rankKeyEvents(candidates, options, peakLookup) {
|
|
8267
8341
|
const scored = [];
|
|
@@ -8790,7 +8864,7 @@ function padBbox(bbox, padding) {
|
|
|
8790
8864
|
//#endregion
|
|
8791
8865
|
//#region src/pipeline-analytics/pipeline/capture-crop.ts
|
|
8792
8866
|
function createCaptureCrop(deps) {
|
|
8793
|
-
return async (frameHandle, bbox, frameWidth, frameHeight, padding, maxWidth) => {
|
|
8867
|
+
return async (frameHandle, bbox, frameWidth, frameHeight, padding, maxWidth, deviceId) => {
|
|
8794
8868
|
const paddedNorm = padBbox({
|
|
8795
8869
|
x: bbox.x / frameWidth,
|
|
8796
8870
|
y: bbox.y / frameHeight,
|
|
@@ -8803,7 +8877,10 @@ function createCaptureCrop(deps) {
|
|
|
8803
8877
|
return nativeCrop;
|
|
8804
8878
|
}
|
|
8805
8879
|
deps.bumpCropMetric(false);
|
|
8806
|
-
deps.logger.debug("enrichment crop native miss — detail scheduler will re-run", {
|
|
8880
|
+
deps.logger.debug("enrichment crop native miss — detail scheduler will re-run", {
|
|
8881
|
+
...deviceId !== void 0 ? { tags: { deviceId } } : {},
|
|
8882
|
+
meta: { nodeId: frameHandle.nodeId }
|
|
8883
|
+
});
|
|
8807
8884
|
return null;
|
|
8808
8885
|
};
|
|
8809
8886
|
}
|
|
@@ -14534,11 +14611,15 @@ function resolveTrackingSettings(raw) {
|
|
|
14534
14611
|
* said "Ships DORMANT: `enabled` defaults to `false`" — that was stale, and on
|
|
14535
14612
|
* 2026-07-30 it nearly produced the conclusion that the gate was not running at
|
|
14536
14613
|
* all. It is: it suppressed several phantom births on device 615 that same day.
|
|
14537
|
-
* Read the schema, not this paragraph.
|
|
14538
|
-
*
|
|
14539
|
-
*
|
|
14540
|
-
*
|
|
14541
|
-
*
|
|
14614
|
+
* Read the schema, not this paragraph.
|
|
14615
|
+
*
|
|
14616
|
+
* The gate is fail-DEFERRED, not fail-open (changed 2026-08-01). Anything that
|
|
14617
|
+
* prevents a MEASUREMENT — a missing frame handle, an unavailable inference
|
|
14618
|
+
* cap, a crop-fetch miss, a re-detection error, a timeout — leaves the birth
|
|
14619
|
+
* UNDECIDED and re-tried on later frames. Only an exhausted deferral with no
|
|
14620
|
+
* crop at all falls open, and that is logged at `warn`. Fail-open on every one
|
|
14621
|
+
* of those paths is what let a brick wall onto camera 636's track feed as a
|
|
14622
|
+
* `vehicle`, and left 45% of births unmeasured over twelve hours.
|
|
14542
14623
|
*
|
|
14543
14624
|
* Every field is independently overridable per camera; an unknown/invalid value
|
|
14544
14625
|
* falls back to the field default (never throws on a bad blob) — mirrors
|
|
@@ -14555,7 +14636,9 @@ var CONFIRMATION_GATE_KEYS = {
|
|
|
14555
14636
|
enabled: "confirmationGateEnabled",
|
|
14556
14637
|
minConfidence: "confirmationGateMinConfidence",
|
|
14557
14638
|
minCropPx: "confirmationGateMinCropPx",
|
|
14558
|
-
timeoutMs: "confirmationGateTimeoutMs"
|
|
14639
|
+
timeoutMs: "confirmationGateTimeoutMs",
|
|
14640
|
+
maxDeferralMs: "confirmationGateMaxDeferralMs",
|
|
14641
|
+
maxAttempts: "confirmationGateMaxAttempts"
|
|
14559
14642
|
};
|
|
14560
14643
|
var ConfirmationGateSettingsSchema = object({
|
|
14561
14644
|
/** Master switch. DEFAULT ON (2026-07-20 rollout) — the gate is FAIL-OPEN (any
|
|
@@ -14577,11 +14660,21 @@ var ConfirmationGateSettingsSchema = object({
|
|
|
14577
14660
|
*/
|
|
14578
14661
|
minCropPx: number().int().min(0).default(48),
|
|
14579
14662
|
/**
|
|
14580
|
-
* Per-
|
|
14581
|
-
* not resolve within this window the
|
|
14582
|
-
*
|
|
14663
|
+
* Per-ATTEMPT confirmation budget (ms). If the crop fetch + re-detection does
|
|
14664
|
+
* not resolve within this window the attempt ends UNDECIDED, so the
|
|
14665
|
+
* synchronous frame path never stalls on inference.
|
|
14583
14666
|
*/
|
|
14584
|
-
timeoutMs: number().int().min(1).default(300)
|
|
14667
|
+
timeoutMs: number().int().min(1).default(300),
|
|
14668
|
+
/**
|
|
14669
|
+
* How long a birth may stay UNDECIDED before the gate stops waiting for a
|
|
14670
|
+
* native crop and decides on the sub-native fallback.
|
|
14671
|
+
*
|
|
14672
|
+
* Measured from the FIRST attempt, so retries cannot push it out. 0 = decide
|
|
14673
|
+
* on the first attempt (the pre-2026-08-01 cadence, without the fail-open).
|
|
14674
|
+
*/
|
|
14675
|
+
maxDeferralMs: number().int().min(0).default(2e3),
|
|
14676
|
+
/** How many gate attempts one birth may have, the first included. */
|
|
14677
|
+
maxAttempts: number().int().min(1).default(4)
|
|
14585
14678
|
});
|
|
14586
14679
|
var CONFIRMATION_GATE_DEFAULTS = ConfirmationGateSettingsSchema.parse({});
|
|
14587
14680
|
/**
|
|
@@ -14598,7 +14691,9 @@ function resolveConfirmationGateSettings(raw) {
|
|
|
14598
14691
|
enabled: s.enabled.catch(CONFIRMATION_GATE_DEFAULTS.enabled).parse(raw[CONFIRMATION_GATE_KEYS.enabled]),
|
|
14599
14692
|
minConfidence: s.minConfidence.catch(CONFIRMATION_GATE_DEFAULTS.minConfidence).parse(raw[CONFIRMATION_GATE_KEYS.minConfidence]),
|
|
14600
14693
|
minCropPx: s.minCropPx.catch(CONFIRMATION_GATE_DEFAULTS.minCropPx).parse(raw[CONFIRMATION_GATE_KEYS.minCropPx]),
|
|
14601
|
-
timeoutMs: s.timeoutMs.catch(CONFIRMATION_GATE_DEFAULTS.timeoutMs).parse(raw[CONFIRMATION_GATE_KEYS.timeoutMs])
|
|
14694
|
+
timeoutMs: s.timeoutMs.catch(CONFIRMATION_GATE_DEFAULTS.timeoutMs).parse(raw[CONFIRMATION_GATE_KEYS.timeoutMs]),
|
|
14695
|
+
maxDeferralMs: s.maxDeferralMs.catch(CONFIRMATION_GATE_DEFAULTS.maxDeferralMs).parse(raw[CONFIRMATION_GATE_KEYS.maxDeferralMs]),
|
|
14696
|
+
maxAttempts: s.maxAttempts.catch(CONFIRMATION_GATE_DEFAULTS.maxAttempts).parse(raw[CONFIRMATION_GATE_KEYS.maxAttempts])
|
|
14602
14697
|
};
|
|
14603
14698
|
}
|
|
14604
14699
|
//#endregion
|
|
@@ -14630,9 +14725,9 @@ function isConfirmationCompatible(trackClassName, detectionMacroClass) {
|
|
|
14630
14725
|
if (track === "other" || det === "other") return true;
|
|
14631
14726
|
return track === det;
|
|
14632
14727
|
}
|
|
14633
|
-
var
|
|
14728
|
+
var undecided = (candidate, reason) => ({
|
|
14634
14729
|
trackId: candidate.trackId,
|
|
14635
|
-
|
|
14730
|
+
verdict: "undecided",
|
|
14636
14731
|
reason,
|
|
14637
14732
|
className: candidate.className
|
|
14638
14733
|
});
|
|
@@ -14648,11 +14743,12 @@ function withTimeout(promise, timeoutMs) {
|
|
|
14648
14743
|
});
|
|
14649
14744
|
});
|
|
14650
14745
|
}
|
|
14651
|
-
async function runConfirmation(candidate, config, deps) {
|
|
14652
|
-
|
|
14653
|
-
if (!crop)
|
|
14746
|
+
async function runConfirmation(candidate, config, deps, exhausted) {
|
|
14747
|
+
let crop = await deps.fetchCrop(candidate);
|
|
14748
|
+
if (!crop && exhausted && deps.fetchFallbackCrop) crop = await deps.fetchFallbackCrop(candidate);
|
|
14749
|
+
if (!crop) return undecided(candidate, "no-crop");
|
|
14654
14750
|
const detections = await deps.redetect(crop);
|
|
14655
|
-
if (detections === null) return
|
|
14751
|
+
if (detections === null) return undecided(candidate, "redetect-error");
|
|
14656
14752
|
let best;
|
|
14657
14753
|
let bestIncompatible;
|
|
14658
14754
|
for (const d of detections) if (isConfirmationCompatible(candidate.className, d.macroClass)) {
|
|
@@ -14661,7 +14757,7 @@ async function runConfirmation(candidate, config, deps) {
|
|
|
14661
14757
|
const confirmed = best !== void 0 && best.score >= config.minConfidence;
|
|
14662
14758
|
return {
|
|
14663
14759
|
trackId: candidate.trackId,
|
|
14664
|
-
confirmed,
|
|
14760
|
+
verdict: confirmed ? "confirmed" : "suppressed",
|
|
14665
14761
|
reason: confirmed ? "confirmed" : "suppressed",
|
|
14666
14762
|
className: candidate.className,
|
|
14667
14763
|
...best ? { bestScore: best.score } : {},
|
|
@@ -14671,31 +14767,40 @@ async function runConfirmation(candidate, config, deps) {
|
|
|
14671
14767
|
} : {}
|
|
14672
14768
|
};
|
|
14673
14769
|
}
|
|
14674
|
-
async function confirmOne(candidate, config, deps) {
|
|
14770
|
+
async function confirmOne(candidate, config, deps, exhausted) {
|
|
14675
14771
|
const cropPx = Math.max(candidate.bbox.w, candidate.bbox.h);
|
|
14676
|
-
if (config.minCropPx > 0 && cropPx < config.minCropPx) return
|
|
14772
|
+
if (config.minCropPx > 0 && cropPx < config.minCropPx) return undecided(candidate, "below-min-crop");
|
|
14677
14773
|
try {
|
|
14678
|
-
return await withTimeout(runConfirmation(candidate, config, deps), config.timeoutMs);
|
|
14774
|
+
return await withTimeout(runConfirmation(candidate, config, deps, exhausted), config.timeoutMs);
|
|
14679
14775
|
} catch {
|
|
14680
|
-
return
|
|
14776
|
+
return undecided(candidate, "timeout");
|
|
14681
14777
|
}
|
|
14682
14778
|
}
|
|
14683
14779
|
/**
|
|
14684
|
-
* Confirm a batch of birth candidates CONCURRENTLY
|
|
14685
|
-
*
|
|
14686
|
-
*
|
|
14687
|
-
*
|
|
14688
|
-
*
|
|
14780
|
+
* Confirm a batch of birth candidates CONCURRENTLY. When the gate is disabled
|
|
14781
|
+
* (or there are no candidates) every candidate is confirmed — byte-identical to
|
|
14782
|
+
* no gate. The caller runs this once, then processes the confirmed births,
|
|
14783
|
+
* defers the undecided ones, and retracts the rest, preserving the original
|
|
14784
|
+
* birth-loop ordering.
|
|
14689
14785
|
*/
|
|
14690
|
-
async function confirmBirths(candidates, config, deps) {
|
|
14691
|
-
if (!config.enabled || candidates.length === 0) return
|
|
14692
|
-
|
|
14786
|
+
async function confirmBirths(candidates, config, deps, exhaustedIds = /* @__PURE__ */ new Set()) {
|
|
14787
|
+
if (!config.enabled || candidates.length === 0) return {
|
|
14788
|
+
confirmed: new Set(candidates.map((c) => c.trackId)),
|
|
14789
|
+
undecided: /* @__PURE__ */ new Set()
|
|
14790
|
+
};
|
|
14791
|
+
const decisions = await Promise.all(candidates.map((c) => confirmOne(c, config, deps, exhaustedIds.has(c.trackId))));
|
|
14693
14792
|
const confirmed = /* @__PURE__ */ new Set();
|
|
14793
|
+
const pending = /* @__PURE__ */ new Set();
|
|
14694
14794
|
for (const decision of decisions) {
|
|
14695
14795
|
deps.onDecision?.(decision);
|
|
14696
|
-
if (decision.confirmed) confirmed.add(decision.trackId);
|
|
14796
|
+
if (decision.verdict === "confirmed") confirmed.add(decision.trackId);
|
|
14797
|
+
else if (decision.verdict === "undecided") if (exhaustedIds.has(decision.trackId)) confirmed.add(decision.trackId);
|
|
14798
|
+
else pending.add(decision.trackId);
|
|
14697
14799
|
}
|
|
14698
|
-
return
|
|
14800
|
+
return {
|
|
14801
|
+
confirmed,
|
|
14802
|
+
undecided: pending
|
|
14803
|
+
};
|
|
14699
14804
|
}
|
|
14700
14805
|
//#endregion
|
|
14701
14806
|
//#region src/pipeline-analytics/face-settings.ts
|
|
@@ -15314,7 +15419,7 @@ function buildDetectionSettingsSections() {
|
|
|
15314
15419
|
{
|
|
15315
15420
|
id: "confirmation-gate",
|
|
15316
15421
|
title: "Confirmation gate",
|
|
15317
|
-
description: "Before a NEW track is born,
|
|
15422
|
+
description: "Before a NEW track is born, 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-DEFERRED: a crop miss, unavailable inference, or a timeout leaves the birth UNDECIDED and looks again on later frames — it is not confirmed by default. Only an exhausted deferral with no crop at all lets a birth through unmeasured.",
|
|
15318
15423
|
columns: 2,
|
|
15319
15424
|
fields: [
|
|
15320
15425
|
{
|
|
@@ -15339,7 +15444,7 @@ function buildDetectionSettingsSections() {
|
|
|
15339
15444
|
type: "slider",
|
|
15340
15445
|
key: CONFIRMATION_GATE_KEYS.minCropPx,
|
|
15341
15446
|
label: "Min crop size",
|
|
15342
|
-
description: "Subject boxes smaller than this (longest side, detection-frame px) are too tiny to confirm reliably — the gate
|
|
15447
|
+
description: "Subject boxes smaller than this (longest side, detection-frame px) are too tiny to confirm reliably — the gate defers them and looks again as the subject approaches and the box grows. 0 = confirm every birth regardless of size.",
|
|
15343
15448
|
min: 0,
|
|
15344
15449
|
max: 256,
|
|
15345
15450
|
step: 8,
|
|
@@ -15351,13 +15456,36 @@ function buildDetectionSettingsSections() {
|
|
|
15351
15456
|
type: "slider",
|
|
15352
15457
|
key: CONFIRMATION_GATE_KEYS.timeoutMs,
|
|
15353
15458
|
label: "Confirmation timeout",
|
|
15354
|
-
description: "Per-
|
|
15459
|
+
description: "Per-ATTEMPT budget for crop fetch + re-detection. If it does not resolve in time the attempt ends undecided, so the frame path never stalls on inference.",
|
|
15355
15460
|
min: 50,
|
|
15356
15461
|
max: 2e3,
|
|
15357
15462
|
step: 50,
|
|
15358
15463
|
default: CONFIRMATION_GATE_DEFAULTS.timeoutMs,
|
|
15359
15464
|
showValue: true,
|
|
15360
15465
|
unit: "ms"
|
|
15466
|
+
},
|
|
15467
|
+
{
|
|
15468
|
+
type: "slider",
|
|
15469
|
+
key: CONFIRMATION_GATE_KEYS.maxDeferralMs,
|
|
15470
|
+
label: "Max deferral",
|
|
15471
|
+
description: "How long an undecided birth may wait for a native crop before the gate decides on the sub-native fallback. Measured from the first attempt, so retries cannot push it out. 0 = decide on the first attempt.",
|
|
15472
|
+
min: 0,
|
|
15473
|
+
max: 1e4,
|
|
15474
|
+
step: 250,
|
|
15475
|
+
default: CONFIRMATION_GATE_DEFAULTS.maxDeferralMs,
|
|
15476
|
+
showValue: true,
|
|
15477
|
+
unit: "ms"
|
|
15478
|
+
},
|
|
15479
|
+
{
|
|
15480
|
+
type: "slider",
|
|
15481
|
+
key: CONFIRMATION_GATE_KEYS.maxAttempts,
|
|
15482
|
+
label: "Max gate attempts",
|
|
15483
|
+
description: "How many times one birth may be put through the gate, the first attempt included. The deferral ends on this or on the max deferral, whichever comes first.",
|
|
15484
|
+
min: 1,
|
|
15485
|
+
max: 12,
|
|
15486
|
+
step: 1,
|
|
15487
|
+
default: CONFIRMATION_GATE_DEFAULTS.maxAttempts,
|
|
15488
|
+
showValue: true
|
|
15361
15489
|
}
|
|
15362
15490
|
]
|
|
15363
15491
|
},
|
|
@@ -20289,6 +20417,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
20289
20417
|
/** See `pipeline/suppressed-births.ts` — rejected births must not be
|
|
20290
20418
|
* re-upserted, and must be forgotten when the tracker drops the id. */
|
|
20291
20419
|
suppressedBirths = new SuppressedBirthRegistry();
|
|
20420
|
+
/** Births the gate could not MEASURE, awaiting another look on a later frame. */
|
|
20421
|
+
deferredBirths = new DeferredBirthRegistry();
|
|
20292
20422
|
lastFrameDimsByDevice = /* @__PURE__ */ new Map();
|
|
20293
20423
|
lastAudioInsertByDevice = /* @__PURE__ */ new Map();
|
|
20294
20424
|
lastMotionInsertByDevice = /* @__PURE__ */ new Map();
|
|
@@ -20374,6 +20504,15 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
20374
20504
|
* the same live-frame window as the face/plate/event-media captures. The
|
|
20375
20505
|
* optional `maxWidth` caps the native crop width (used for the full-frame key
|
|
20376
20506
|
* frame so a 4K native surface never floods the transport). */
|
|
20507
|
+
/**
|
|
20508
|
+
* The BOUNDED fallback `captureCrop` refuses — same padded ROI out of the
|
|
20509
|
+
* retained full frame (keyframe-native tier, or the runner's ≤640 RAM tier;
|
|
20510
|
+
* honest sub-native, NEVER upscaled). Built for the gallery tiles; the
|
|
20511
|
+
* confirmation gate borrows it on an EXHAUSTED deferral only. See
|
|
20512
|
+
* `docs/decisions/` — the gate is a model input, so this is a deliberate
|
|
20513
|
+
* exception to "model-input crops keep captureCrop".
|
|
20514
|
+
*/
|
|
20515
|
+
captureDisplayCropFn = null;
|
|
20377
20516
|
captureCrop = null;
|
|
20378
20517
|
/** Full NATIVE frame by handle (downscaled worker-side to `maxWidth`), routed
|
|
20379
20518
|
* to the owning runner, PLUS the source `tier`. Captured in the async init
|
|
@@ -20772,6 +20911,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
20772
20911
|
* route through the CaptureScheduler at the injection point (S4). */
|
|
20773
20912
|
buildRecognizers(api, logger, stores, transport) {
|
|
20774
20913
|
const captureDisplayCrop = transport.captureDisplayCrop;
|
|
20914
|
+
this.captureDisplayCropFn = captureDisplayCrop;
|
|
20775
20915
|
this.faceRecognizer = new FaceRecognizer({
|
|
20776
20916
|
identityStore: stores.identityStore,
|
|
20777
20917
|
faceStore: stores.faceStore,
|
|
@@ -21430,7 +21570,19 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
21430
21570
|
t
|
|
21431
21571
|
});
|
|
21432
21572
|
}
|
|
21433
|
-
const
|
|
21573
|
+
for (const id of this.deferredBirths.deferredIds(key)) {
|
|
21574
|
+
if (bornCandidates.some((c) => c.id === id)) continue;
|
|
21575
|
+
const t = result.tracked.find((x) => x.trackId === id);
|
|
21576
|
+
if (t) bornCandidates.push({
|
|
21577
|
+
id,
|
|
21578
|
+
t
|
|
21579
|
+
});
|
|
21580
|
+
}
|
|
21581
|
+
const gateSettings = await this.resolveDeviceConfirmationGateSettings(deviceId);
|
|
21582
|
+
const exhaustionNowMs = Date.now();
|
|
21583
|
+
const exhaustedIds = /* @__PURE__ */ new Set();
|
|
21584
|
+
for (const { id } of bornCandidates) if (this.deferredBirths.exhausted(key, id, exhaustionNowMs, gateSettings.maxAttempts, gateSettings.maxDeferralMs)) exhaustedIds.add(id);
|
|
21585
|
+
const outcome = await this.confirmTrackBirths(bornCandidates.map(({ id, t }) => ({
|
|
21434
21586
|
trackId: id,
|
|
21435
21587
|
className: t.className,
|
|
21436
21588
|
bbox: t.bbox
|
|
@@ -21439,18 +21591,53 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
21439
21591
|
frameHandle,
|
|
21440
21592
|
frameWidth: result.frameWidth,
|
|
21441
21593
|
frameHeight: result.frameHeight
|
|
21442
|
-
});
|
|
21594
|
+
}, exhaustedIds);
|
|
21595
|
+
const gateNowMs = Date.now();
|
|
21443
21596
|
for (const { id, t } of bornCandidates) {
|
|
21444
|
-
if (
|
|
21597
|
+
if (outcome.undecided.has(id)) {
|
|
21598
|
+
this.deferredBirths.defer(key, id, gateNowMs);
|
|
21599
|
+
log.info("birth undecided — deferred for another look", { meta: {
|
|
21600
|
+
trackId: id,
|
|
21601
|
+
className: t.className,
|
|
21602
|
+
attempts: this.deferredBirths.attemptsFor(key, id),
|
|
21603
|
+
elapsedMs: this.deferredBirths.elapsedMs(key, id, gateNowMs),
|
|
21604
|
+
source
|
|
21605
|
+
} });
|
|
21606
|
+
continue;
|
|
21607
|
+
}
|
|
21608
|
+
const wasDeferred = this.deferredBirths.isDeferred(key, id);
|
|
21609
|
+
const deferredForMs = this.deferredBirths.elapsedMs(key, id, gateNowMs);
|
|
21610
|
+
const deferredAttempts = this.deferredBirths.attemptsFor(key, id);
|
|
21611
|
+
this.deferredBirths.resolve(key, id);
|
|
21612
|
+
if (!outcome.confirmed.has(id)) {
|
|
21445
21613
|
this.suppressedBirths.reject(key, id);
|
|
21446
21614
|
this.trackStore?.dropActive(id);
|
|
21447
21615
|
log.info("birth suppressed — track record retracted", { meta: {
|
|
21448
21616
|
trackId: id,
|
|
21449
21617
|
className: t.className,
|
|
21450
|
-
source
|
|
21618
|
+
source,
|
|
21619
|
+
...wasDeferred ? {
|
|
21620
|
+
wasDeferred,
|
|
21621
|
+
deferredForMs,
|
|
21622
|
+
deferredAttempts
|
|
21623
|
+
} : {}
|
|
21451
21624
|
} });
|
|
21452
21625
|
continue;
|
|
21453
21626
|
}
|
|
21627
|
+
if (wasDeferred && exhaustedIds.has(id)) log.warn("birth allowed unmeasured — deferral exhausted, no crop available", { meta: {
|
|
21628
|
+
trackId: id,
|
|
21629
|
+
className: t.className,
|
|
21630
|
+
source,
|
|
21631
|
+
deferredForMs,
|
|
21632
|
+
deferredAttempts
|
|
21633
|
+
} });
|
|
21634
|
+
else if (wasDeferred) log.info("birth decided late — confirmed after deferral", { meta: {
|
|
21635
|
+
trackId: id,
|
|
21636
|
+
className: t.className,
|
|
21637
|
+
source,
|
|
21638
|
+
deferredForMs,
|
|
21639
|
+
deferredAttempts
|
|
21640
|
+
} });
|
|
21454
21641
|
newTrackCount += 1;
|
|
21455
21642
|
log.info("track started", { meta: {
|
|
21456
21643
|
trackId: id,
|
|
@@ -21512,6 +21699,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
21512
21699
|
} });
|
|
21513
21700
|
}
|
|
21514
21701
|
this.suppressedBirths.retain(key, currentTrackIds);
|
|
21702
|
+
this.deferredBirths.retain(key, currentTrackIds);
|
|
21515
21703
|
this.lastActiveTrackIds.set(key, currentTrackIds);
|
|
21516
21704
|
const stationarySettings = this.stationarySettingsFromCache(deviceId);
|
|
21517
21705
|
if (source === "pipeline" && this.stationaryRegistry && stationarySettings.enabled) {
|
|
@@ -21940,19 +22128,25 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
21940
22128
|
* candidate) and each failure path fails OPEN, so the synchronous frame path
|
|
21941
22129
|
* is never blocked or reordered by a slow/failed re-detection.
|
|
21942
22130
|
*/
|
|
21943
|
-
async confirmTrackBirths(candidates, params) {
|
|
21944
|
-
const allConfirmed = () =>
|
|
22131
|
+
async confirmTrackBirths(candidates, params, exhaustedIds = /* @__PURE__ */ new Set()) {
|
|
22132
|
+
const allConfirmed = () => ({
|
|
22133
|
+
confirmed: new Set(candidates.map((c) => c.trackId)),
|
|
22134
|
+
undecided: /* @__PURE__ */ new Set()
|
|
22135
|
+
});
|
|
21945
22136
|
if (candidates.length === 0) return allConfirmed();
|
|
21946
22137
|
const config = await this.resolveDeviceConfirmationGateSettings(params.deviceId);
|
|
21947
22138
|
if (!config.enabled) return allConfirmed();
|
|
21948
22139
|
const { frameHandle, frameWidth, frameHeight, deviceId } = params;
|
|
21949
22140
|
const captureCrop = this.captureCrop;
|
|
21950
22141
|
if (!frameHandle || !captureCrop || frameWidth <= 0 || frameHeight <= 0) {
|
|
21951
|
-
this.ctx.logger.
|
|
22142
|
+
this.ctx.logger.info("confirmation gate: no crop path — births allowed unmeasured", {
|
|
21952
22143
|
tags: { deviceId },
|
|
21953
22144
|
meta: {
|
|
21954
22145
|
candidates: candidates.length,
|
|
21955
|
-
hasHandle: Boolean(frameHandle)
|
|
22146
|
+
hasHandle: Boolean(frameHandle),
|
|
22147
|
+
hasCaptureCrop: Boolean(captureCrop),
|
|
22148
|
+
frameWidth,
|
|
22149
|
+
frameHeight
|
|
21956
22150
|
}
|
|
21957
22151
|
});
|
|
21958
22152
|
return allConfirmed();
|
|
@@ -21968,8 +22162,18 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
21968
22162
|
y: candidate.bbox.y,
|
|
21969
22163
|
w: candidate.bbox.w,
|
|
21970
22164
|
h: candidate.bbox.h
|
|
21971
|
-
}, frameWidth, frameHeight, CONFIRMATION_CROP_PADDING, CONFIRMATION_CROP_MAX_WIDTH)
|
|
22165
|
+
}, frameWidth, frameHeight, CONFIRMATION_CROP_PADDING, CONFIRMATION_CROP_MAX_WIDTH, deviceId)
|
|
21972
22166
|
}),
|
|
22167
|
+
fetchFallbackCrop: (candidate) => {
|
|
22168
|
+
const displayCrop = this.captureDisplayCropFn;
|
|
22169
|
+
if (!displayCrop) return Promise.resolve(null);
|
|
22170
|
+
return displayCrop(frameHandle, {
|
|
22171
|
+
x: candidate.bbox.x,
|
|
22172
|
+
y: candidate.bbox.y,
|
|
22173
|
+
w: candidate.bbox.w,
|
|
22174
|
+
h: candidate.bbox.h
|
|
22175
|
+
}, frameWidth, frameHeight, CONFIRMATION_CROP_PADDING, candidate.trackId, CONFIRMATION_CROP_MAX_WIDTH);
|
|
22176
|
+
},
|
|
21973
22177
|
redetect: (cropJpeg) => this.redetectCropForConfirmation(nodeId, deviceId, cropJpeg),
|
|
21974
22178
|
onDecision: (decision) => {
|
|
21975
22179
|
const meta = {
|
|
@@ -21983,20 +22187,28 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
21983
22187
|
} : {},
|
|
21984
22188
|
minConfidence: config.minConfidence
|
|
21985
22189
|
};
|
|
21986
|
-
|
|
21987
|
-
|
|
21988
|
-
|
|
21989
|
-
|
|
21990
|
-
|
|
21991
|
-
|
|
21992
|
-
|
|
21993
|
-
|
|
21994
|
-
|
|
21995
|
-
|
|
21996
|
-
|
|
21997
|
-
|
|
22190
|
+
switch (decision.verdict) {
|
|
22191
|
+
case "suppressed":
|
|
22192
|
+
this.ctx.logger.info("confirmation gate: birth suppressed (false positive)", {
|
|
22193
|
+
tags: { deviceId },
|
|
22194
|
+
meta
|
|
22195
|
+
});
|
|
22196
|
+
break;
|
|
22197
|
+
case "undecided":
|
|
22198
|
+
this.ctx.logger.info("confirmation gate: birth undecided", {
|
|
22199
|
+
tags: { deviceId },
|
|
22200
|
+
meta
|
|
22201
|
+
});
|
|
22202
|
+
break;
|
|
22203
|
+
case "confirmed":
|
|
22204
|
+
this.ctx.logger.info("confirmation gate: birth confirmed", {
|
|
22205
|
+
tags: { deviceId },
|
|
22206
|
+
meta
|
|
22207
|
+
});
|
|
22208
|
+
break;
|
|
22209
|
+
}
|
|
21998
22210
|
}
|
|
21999
|
-
});
|
|
22211
|
+
}, exhaustedIds);
|
|
22000
22212
|
}
|
|
22001
22213
|
async resolveGlobalFaceEnabled() {
|
|
22002
22214
|
return this.faceGlobalEnabledCache.get(() => this.faceGlobalEnabledState.get());
|
|
@@ -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-DM0rHYe_.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.2.
|
|
3
|
+
"version": "1.2.22",
|
|
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",
|