@camstack/addon-post-analysis 1.1.35 → 1.1.37
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-Bc4L7FGf.js → dist-CThBV9dq.js} +0 -55
- package/dist/{dist-8DTQLWKO.mjs → dist-DPBet4IQ.mjs} +0 -55
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/{node-BRwocT7C.js → node-CDbDhtGa.js} +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CMH9Gs68.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-oxNx9gzC.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-DdC1GJfs.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-Bhr2CNHt.mjs → hostInit-Do2pU2J8.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +354 -38
- package/dist/pipeline-analytics/index.mjs +353 -37
- 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-DLzy78dH.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-DPBet4IQ.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import sharp from "sharp";
|
|
4
4
|
//#region src/pipeline-analytics/videoclips-provider.ts
|
|
@@ -2132,6 +2132,29 @@ function isEdgeClear(input) {
|
|
|
2132
2132
|
return true;
|
|
2133
2133
|
}
|
|
2134
2134
|
/**
|
|
2135
|
+
* Centeredness of a bbox: 1 when the subject's centre sits exactly at the frame
|
|
2136
|
+
* centre, decaying toward 0 as it approaches a corner. Pure geometry (no pixels)
|
|
2137
|
+
* — a cheap proxy for "is the subject well-framed?" used as the best-frame
|
|
2138
|
+
* tie-breaker so a low-importance short track stops locking in an edge-of-frame
|
|
2139
|
+
* subject when a better-centred, near-equal-confidence frame is available.
|
|
2140
|
+
*
|
|
2141
|
+
* The score is `1 - normalizedDistance(centre → frameCentre)`, where the
|
|
2142
|
+
* distance is normalised by the max possible (centre → corner) so it is
|
|
2143
|
+
* scale-invariant. Degenerate/unknown dims (≤ 0) return 1 (neutral — the gate
|
|
2144
|
+
* falls back to pure confidence, matching {@link isEdgeClear}).
|
|
2145
|
+
*/
|
|
2146
|
+
function bboxCenterScore(bbox, frameWidth, frameHeight) {
|
|
2147
|
+
if (frameWidth <= 0 || frameHeight <= 0) return 1;
|
|
2148
|
+
const cx = bbox.x + bbox.w / 2;
|
|
2149
|
+
const cy = bbox.y + bbox.h / 2;
|
|
2150
|
+
const fcx = frameWidth / 2;
|
|
2151
|
+
const fcy = frameHeight / 2;
|
|
2152
|
+
const dx = (cx - fcx) / fcx;
|
|
2153
|
+
const dy = (cy - fcy) / fcy;
|
|
2154
|
+
const dist = Math.hypot(dx, dy) / Math.SQRT2;
|
|
2155
|
+
return Math.max(0, Math.min(1, 1 - dist));
|
|
2156
|
+
}
|
|
2157
|
+
/**
|
|
2135
2158
|
* Edge-aware "is `candidate` a new best over `current`?" comparator.
|
|
2136
2159
|
*
|
|
2137
2160
|
* Tier order: edge-clear ALWAYS outranks edge-touching (a whole subject beats a
|
|
@@ -2139,13 +2162,25 @@ function isEdgeClear(input) {
|
|
|
2139
2162
|
* confidence past the `hysteresis` margin wins. The tier upgrade
|
|
2140
2163
|
* (touching → clear) bypasses hysteresis — the first clear frame is always taken.
|
|
2141
2164
|
*
|
|
2165
|
+
* CENTERING TIE-BREAK (#27-D): within the same tier, when neither frame clearly
|
|
2166
|
+
* wins on confidence (the two are within the `hysteresis` band) but the
|
|
2167
|
+
* candidate is meaningfully better CENTRED ({@link CENTER_TIE_BREAK_MARGIN}), the
|
|
2168
|
+
* candidate wins. This only engages when both sides carry a `centerScore` (the
|
|
2169
|
+
* best-frame path), so low-importance short tracks stop keeping an edge-of-frame
|
|
2170
|
+
* subject over an equally-confident, better-framed one. The face /
|
|
2171
|
+
* object-embedding callers omit `centerScore` → identical legacy behaviour.
|
|
2172
|
+
*
|
|
2142
2173
|
* Time gating (`minGapMs`) is applied by the caller (`BestDetectionTracker`),
|
|
2143
2174
|
* not here, so this stays a pure value comparison.
|
|
2144
2175
|
*/
|
|
2145
2176
|
function isEdgeAwareNewBest(current, candidate, hysteresis) {
|
|
2146
2177
|
if (candidate.edgeClear && !current.edgeClear) return true;
|
|
2147
2178
|
if (!candidate.edgeClear && current.edgeClear) return false;
|
|
2148
|
-
|
|
2179
|
+
if (candidate.confidence > current.confidence + hysteresis) return true;
|
|
2180
|
+
if (candidate.centerScore !== void 0 && current.centerScore !== void 0) {
|
|
2181
|
+
if (Math.abs(candidate.confidence - current.confidence) <= hysteresis && candidate.centerScore > current.centerScore + .1) return true;
|
|
2182
|
+
}
|
|
2183
|
+
return false;
|
|
2149
2184
|
}
|
|
2150
2185
|
//#endregion
|
|
2151
2186
|
//#region src/pipeline-analytics/pipeline/best-detection-tracker.ts
|
|
@@ -2181,6 +2216,10 @@ var BestDetectionTracker = class {
|
|
|
2181
2216
|
* the edge tier is not in play for the track (treated as clear → the legacy
|
|
2182
2217
|
* pure-confidence policy). */
|
|
2183
2218
|
edgeClear = /* @__PURE__ */ new Map();
|
|
2219
|
+
/** Held peak's centeredness (0..1), PARALLEL to `best`. Absent = the caller
|
|
2220
|
+
* does not supply centering (face / object-embedding paths) → the centering
|
|
2221
|
+
* tie-break is disabled and the legacy confidence policy applies. */
|
|
2222
|
+
centerScore = /* @__PURE__ */ new Map();
|
|
2184
2223
|
constructor(options = {}) {
|
|
2185
2224
|
this.hysteresis = options.hysteresis ?? 0;
|
|
2186
2225
|
this.minGapMs = options.minGapMs ?? 0;
|
|
@@ -2197,7 +2236,7 @@ var BestDetectionTracker = class {
|
|
|
2197
2236
|
* the classic policy holds: a confidence past the `hysteresis` margin that also
|
|
2198
2237
|
* respects `minGapMs` wins. On acceptance the held peak advances.
|
|
2199
2238
|
*/
|
|
2200
|
-
observe(trackId, confidence, timestamp, edgeClear) {
|
|
2239
|
+
observe(trackId, confidence, timestamp, edgeClear, centerScore) {
|
|
2201
2240
|
const cur = this.best.get(trackId);
|
|
2202
2241
|
if (cur === void 0) {
|
|
2203
2242
|
this.best.set(trackId, {
|
|
@@ -2205,16 +2244,20 @@ var BestDetectionTracker = class {
|
|
|
2205
2244
|
atMs: timestamp
|
|
2206
2245
|
});
|
|
2207
2246
|
if (edgeClear !== void 0) this.edgeClear.set(trackId, edgeClear);
|
|
2247
|
+
if (centerScore !== void 0) this.centerScore.set(trackId, centerScore);
|
|
2208
2248
|
return true;
|
|
2209
2249
|
}
|
|
2210
2250
|
const curClear = this.edgeClear.get(trackId) ?? true;
|
|
2211
2251
|
const candClear = edgeClear ?? true;
|
|
2252
|
+
const curCenter = this.centerScore.get(trackId);
|
|
2212
2253
|
const isNewBest = candClear && !curClear ? true : isEdgeAwareNewBest({
|
|
2213
2254
|
confidence: cur.confidence,
|
|
2214
|
-
edgeClear: curClear
|
|
2255
|
+
edgeClear: curClear,
|
|
2256
|
+
...curCenter !== void 0 ? { centerScore: curCenter } : {}
|
|
2215
2257
|
}, {
|
|
2216
2258
|
confidence,
|
|
2217
|
-
edgeClear: candClear
|
|
2259
|
+
edgeClear: candClear,
|
|
2260
|
+
...centerScore !== void 0 ? { centerScore } : {}
|
|
2218
2261
|
}, this.hysteresis) && timestamp - cur.atMs >= this.minGapMs;
|
|
2219
2262
|
if (isNewBest) {
|
|
2220
2263
|
this.best.set(trackId, {
|
|
@@ -2222,6 +2265,7 @@ var BestDetectionTracker = class {
|
|
|
2222
2265
|
atMs: timestamp
|
|
2223
2266
|
});
|
|
2224
2267
|
if (edgeClear !== void 0) this.edgeClear.set(trackId, edgeClear);
|
|
2268
|
+
if (centerScore !== void 0) this.centerScore.set(trackId, centerScore);
|
|
2225
2269
|
}
|
|
2226
2270
|
return isNewBest;
|
|
2227
2271
|
}
|
|
@@ -2233,10 +2277,12 @@ var BestDetectionTracker = class {
|
|
|
2233
2277
|
delete(trackId) {
|
|
2234
2278
|
this.best.delete(trackId);
|
|
2235
2279
|
this.edgeClear.delete(trackId);
|
|
2280
|
+
this.centerScore.delete(trackId);
|
|
2236
2281
|
}
|
|
2237
2282
|
clear() {
|
|
2238
2283
|
this.best.clear();
|
|
2239
2284
|
this.edgeClear.clear();
|
|
2285
|
+
this.centerScore.clear();
|
|
2240
2286
|
}
|
|
2241
2287
|
};
|
|
2242
2288
|
//#endregion
|
|
@@ -2389,6 +2435,9 @@ var WAKE_ASSOC_IOU = .1;
|
|
|
2389
2435
|
* long) so a freshly-spawned static blob isn't promoted instantly.
|
|
2390
2436
|
*/
|
|
2391
2437
|
var PROMOTION_WINDOW_MS = 3e4;
|
|
2438
|
+
/** Unconfirmed-entry time-to-live: if no detection confirms an entry for this
|
|
2439
|
+
* long (object removed while unobserved, or a long occlusion) → retire it. */
|
|
2440
|
+
var ENTRY_TTL_MS = 5 * 6e4;
|
|
2392
2441
|
var DEFAULT_MATCH_CONFIG = {
|
|
2393
2442
|
suppressIou: SUPPRESS_IOU,
|
|
2394
2443
|
wakeAssocIou: WAKE_ASSOC_IOU
|
|
@@ -2596,6 +2645,7 @@ var StationaryObjectRegistry = class {
|
|
|
2596
2645
|
logger;
|
|
2597
2646
|
matchConfig;
|
|
2598
2647
|
entryTtlMs;
|
|
2648
|
+
ttlForDevice;
|
|
2599
2649
|
onChange;
|
|
2600
2650
|
/** Latest processed-frame timestamp per device — expiry counts OBSERVED
|
|
2601
2651
|
* time, not wall-clock. A session-dispatch camera produces no frames
|
|
@@ -2607,6 +2657,7 @@ var StationaryObjectRegistry = class {
|
|
|
2607
2657
|
this.logger = deps.logger;
|
|
2608
2658
|
this.matchConfig = deps.matchConfig ?? DEFAULT_MATCH_CONFIG;
|
|
2609
2659
|
this.entryTtlMs = deps.entryTtlMs ?? 3e5;
|
|
2660
|
+
this.ttlForDevice = deps.ttlForDevice;
|
|
2610
2661
|
this.onChange = deps.onChange;
|
|
2611
2662
|
}
|
|
2612
2663
|
static async declare(store) {
|
|
@@ -2662,7 +2713,7 @@ var StationaryObjectRegistry = class {
|
|
|
2662
2713
|
* entries. PURE with respect to registry state — apply the outcome with
|
|
2663
2714
|
* {@link applyFrameOutcome} once the frame result is assembled.
|
|
2664
2715
|
*/
|
|
2665
|
-
filter(input) {
|
|
2716
|
+
filter(input, config) {
|
|
2666
2717
|
const entries = this.list(input.deviceId);
|
|
2667
2718
|
if (entries.length === 0) return {
|
|
2668
2719
|
suppressedIndices: /* @__PURE__ */ new Set(),
|
|
@@ -2672,7 +2723,7 @@ var StationaryObjectRegistry = class {
|
|
|
2672
2723
|
return partitionDetectionsAgainstRegistry({
|
|
2673
2724
|
entries,
|
|
2674
2725
|
detections: input.detections,
|
|
2675
|
-
config: this.matchConfig
|
|
2726
|
+
config: config ?? this.matchConfig
|
|
2676
2727
|
});
|
|
2677
2728
|
}
|
|
2678
2729
|
/** Fold a frame's gate result back into state: advance confirmed entries'
|
|
@@ -2741,7 +2792,8 @@ var StationaryObjectRegistry = class {
|
|
|
2741
2792
|
for (const [deviceId, m] of this.byDevice) {
|
|
2742
2793
|
const observedAt = this.lastFrameAtByDevice.get(deviceId);
|
|
2743
2794
|
if (observedAt === void 0) continue;
|
|
2744
|
-
|
|
2795
|
+
const ttl = this.ttlForDevice?.(deviceId) ?? this.entryTtlMs;
|
|
2796
|
+
for (const [id, e] of m) if (observedAt - e.lastConfirmedAt > ttl) {
|
|
2745
2797
|
m.delete(id);
|
|
2746
2798
|
this.dirty.delete(id);
|
|
2747
2799
|
retired.push(e);
|
|
@@ -2890,6 +2942,55 @@ function computeStationaryEntryZones(entry, zones) {
|
|
|
2890
2942
|
return matched;
|
|
2891
2943
|
}
|
|
2892
2944
|
//#endregion
|
|
2945
|
+
//#region src/pipeline-analytics/stationary-settings.ts
|
|
2946
|
+
/**
|
|
2947
|
+
* Per-device stationary-object (parked/idle suppression) settings. Cascade: a
|
|
2948
|
+
* per-device override on top of the global default, resolved per field (an
|
|
2949
|
+
* invalid/missing value falls back to its default — parse never throws).
|
|
2950
|
+
* Mirrors `media-settings` / `tracking-settings`.
|
|
2951
|
+
*
|
|
2952
|
+
* The defaults are the SAME constants the registry uses at runtime
|
|
2953
|
+
* (`stationary-types.ts`), imported (not copied) so an unset value and a reset
|
|
2954
|
+
* value both resolve to exactly today's behaviour — "reset == unset == today",
|
|
2955
|
+
* with no drift. Guarded by a unit test asserting the equality.
|
|
2956
|
+
*/
|
|
2957
|
+
var StationarySettingsSchema = object({
|
|
2958
|
+
/** Master switch. Off ⇒ no promotion + no suppression for this camera (every
|
|
2959
|
+
* parked object keeps spawning normal tracks). */
|
|
2960
|
+
enabled: boolean().default(true),
|
|
2961
|
+
/** IoU at/above which a detection is the same parked object, unmoved →
|
|
2962
|
+
* suppress its spawn. `SUPPRESS_IOU`. */
|
|
2963
|
+
suppressIou: number().min(.3).max(.9).default(SUPPRESS_IOU),
|
|
2964
|
+
/** Minimum IoU for a detection to be ASSOCIATED with an entry (confirm or
|
|
2965
|
+
* wake it) — the overlap gate that stops a different vehicle from waking a
|
|
2966
|
+
* parked entry (the 617 flood fix). `WAKE_ASSOC_IOU`. */
|
|
2967
|
+
wakeAssocIou: number().min(.02).max(.5).default(WAKE_ASSOC_IOU),
|
|
2968
|
+
/** Recent-window stillness a track must hold to be PROMOTED to a parked
|
|
2969
|
+
* entry (also the minimum track age). `PROMOTION_WINDOW_MS`. */
|
|
2970
|
+
promotionWindowMs: number().int().min(5e3).max(12e4).default(PROMOTION_WINDOW_MS),
|
|
2971
|
+
/** Observed-time TTL: retire an entry unconfirmed for this long (measured on
|
|
2972
|
+
* frames-flowing time, not wall-clock). `ENTRY_TTL_MS`. */
|
|
2973
|
+
entryTtlMs: number().int().min(6e4).max(18e5).default(ENTRY_TTL_MS)
|
|
2974
|
+
});
|
|
2975
|
+
var STATIONARY_DEFAULTS = StationarySettingsSchema.parse({});
|
|
2976
|
+
/**
|
|
2977
|
+
* Resolve a per-device store blob into typed stationary settings. Unknown/invalid
|
|
2978
|
+
* fields fall back to the default for that field (never throws on a bad blob).
|
|
2979
|
+
*/
|
|
2980
|
+
function resolveStationarySettings(raw) {
|
|
2981
|
+
const pick = (key) => {
|
|
2982
|
+
const parsed = StationarySettingsSchema.shape[key].safeParse(raw[key]);
|
|
2983
|
+
return parsed.success ? parsed.data : STATIONARY_DEFAULTS[key];
|
|
2984
|
+
};
|
|
2985
|
+
return {
|
|
2986
|
+
enabled: pick("enabled"),
|
|
2987
|
+
suppressIou: pick("suppressIou"),
|
|
2988
|
+
wakeAssocIou: pick("wakeAssocIou"),
|
|
2989
|
+
promotionWindowMs: pick("promotionWindowMs"),
|
|
2990
|
+
entryTtlMs: pick("entryTtlMs")
|
|
2991
|
+
};
|
|
2992
|
+
}
|
|
2993
|
+
//#endregion
|
|
2893
2994
|
//#region src/pipeline-analytics/pipeline/track-appearance.ts
|
|
2894
2995
|
/**
|
|
2895
2996
|
* Pure: no side effects. `continuing` = still active from last frame;
|
|
@@ -5823,6 +5924,22 @@ async function ingestSensorStateChange(deps, data, timestamp) {
|
|
|
5823
5924
|
}
|
|
5824
5925
|
return inserted;
|
|
5825
5926
|
}
|
|
5927
|
+
/** JPEG quality for the downscaled full frame — matches the crop path. */
|
|
5928
|
+
var FULL_FRAME_QUALITY = 80;
|
|
5929
|
+
/**
|
|
5930
|
+
* Downscale an already-encoded JPEG full frame to FIT WITHIN
|
|
5931
|
+
* {@link FULL_FRAME_MAX_WIDTH}×{@link FULL_FRAME_MAX_HEIGHT}, preserving aspect
|
|
5932
|
+
* ratio (`fit: 'inside'`) and never enlarging a source already smaller than the
|
|
5933
|
+
* box. Re-encodes as JPEG. Used before persisting a synthetic sensor/control
|
|
5934
|
+
* track's whole-scene snapshot so a raw native-resolution frame (a 4K bedroom
|
|
5935
|
+
* at night) is never stored or served — the privacy fix moved to CAPTURE time.
|
|
5936
|
+
*/
|
|
5937
|
+
async function downscaleFullFrameJpeg(jpeg, maxWidth = 640, maxHeight = 360) {
|
|
5938
|
+
return sharp(Buffer.from(jpeg)).resize(maxWidth, maxHeight, {
|
|
5939
|
+
fit: "inside",
|
|
5940
|
+
withoutEnlargement: true
|
|
5941
|
+
}).jpeg({ quality: FULL_FRAME_QUALITY }).toBuffer();
|
|
5942
|
+
}
|
|
5826
5943
|
//#endregion
|
|
5827
5944
|
//#region src/pipeline-analytics/services/synthetic-sensor-track.ts
|
|
5828
5945
|
/**
|
|
@@ -5887,7 +6004,13 @@ var SyntheticSensorTrackMaterializer = class {
|
|
|
5887
6004
|
force: true
|
|
5888
6005
|
});
|
|
5889
6006
|
if (snap !== null) {
|
|
5890
|
-
const
|
|
6007
|
+
const raw = Buffer.from(snap.base64, "base64");
|
|
6008
|
+
let data = raw;
|
|
6009
|
+
try {
|
|
6010
|
+
data = await downscaleFullFrameJpeg(raw);
|
|
6011
|
+
} catch (err) {
|
|
6012
|
+
this.deps.onError?.("downscaleSnapshot", err);
|
|
6013
|
+
}
|
|
5891
6014
|
mediaKey = await this.deps.media.put({
|
|
5892
6015
|
deviceId: input.cameraId,
|
|
5893
6016
|
ownerKind: "track",
|
|
@@ -6106,7 +6229,10 @@ var EventMediaDispatcher = class {
|
|
|
6106
6229
|
async captureForFrame(input) {
|
|
6107
6230
|
const { deviceId, frameHandle, events, trackFrames } = input;
|
|
6108
6231
|
const snapshots = input.snapshots ?? [];
|
|
6109
|
-
const empty = {
|
|
6232
|
+
const empty = {
|
|
6233
|
+
storedSnapshots: [],
|
|
6234
|
+
thumbnailTrackIds: []
|
|
6235
|
+
};
|
|
6110
6236
|
if (events.length === 0 && trackFrames.length === 0 && snapshots.length === 0) return empty;
|
|
6111
6237
|
let decoded;
|
|
6112
6238
|
try {
|
|
@@ -6160,11 +6286,16 @@ var EventMediaDispatcher = class {
|
|
|
6160
6286
|
for (const ev of events) await this.writeEventMedia(deviceId, frameData, fw, fh, ev, input.cropPadding);
|
|
6161
6287
|
for (const tf of trackFrames) await this.writeTrackFrame(deviceId, frameData, fw, fh, tf);
|
|
6162
6288
|
const storedSnapshots = [];
|
|
6289
|
+
const thumbnailTrackIds = [];
|
|
6163
6290
|
for (const sn of snapshots) {
|
|
6164
|
-
const
|
|
6165
|
-
if (
|
|
6291
|
+
const res = await this.writeTrackSnapshot(deviceId, frameData, fw, fh, sn, input.cropPadding);
|
|
6292
|
+
if (res.storedSnapshot) storedSnapshots.push(res.storedSnapshot);
|
|
6293
|
+
if (res.thumbnailWritten) thumbnailTrackIds.push(sn.trackId);
|
|
6166
6294
|
}
|
|
6167
|
-
return {
|
|
6295
|
+
return {
|
|
6296
|
+
storedSnapshots,
|
|
6297
|
+
thumbnailTrackIds
|
|
6298
|
+
};
|
|
6168
6299
|
}
|
|
6169
6300
|
/**
|
|
6170
6301
|
* Periodic per-track media (§5). The boxed FULL frame is encoded once and
|
|
@@ -6175,10 +6306,14 @@ var EventMediaDispatcher = class {
|
|
|
6175
6306
|
* object event, and a full frame there shows the scene (e.g. a foreground
|
|
6176
6307
|
* parked car), not the track's subject. Returns the appended snapshot for
|
|
6177
6308
|
* TrackStore wiring (null when `appendSnapshot` is false or the encode
|
|
6178
|
-
* failed).
|
|
6309
|
+
* failed). `thumbnailWritten` reports whether a best `thumbnail` actually
|
|
6310
|
+
* landed this frame (#27-A) so the caller can stop forcing retries.
|
|
6179
6311
|
*/
|
|
6180
6312
|
async writeTrackSnapshot(deviceId, frameData, fw, fh, sn, cropPadding) {
|
|
6181
|
-
if (!sn.appendSnapshot && !sn.rollingLastFrame && !sn.bestThumbnail) return
|
|
6313
|
+
if (!sn.appendSnapshot && !sn.rollingLastFrame && !sn.bestThumbnail) return {
|
|
6314
|
+
storedSnapshot: null,
|
|
6315
|
+
thumbnailWritten: false
|
|
6316
|
+
};
|
|
6182
6317
|
let boxed = null;
|
|
6183
6318
|
if (sn.appendSnapshot || sn.rollingLastFrame) try {
|
|
6184
6319
|
boxed = await drawBoxedFrame(frameData, fw, fh, [{
|
|
@@ -6213,9 +6348,10 @@ var EventMediaDispatcher = class {
|
|
|
6213
6348
|
};
|
|
6214
6349
|
} catch {}
|
|
6215
6350
|
if (sn.rollingLastFrame && boxed) await this.replaceKind(deviceId, sn.trackId, "lastFrame", sn.timestamp, boxed);
|
|
6351
|
+
let thumbnailWritten = false;
|
|
6216
6352
|
if (sn.bestThumbnail) try {
|
|
6217
6353
|
const crop = await this.cropSubjectRegion(frameData, fw, fh, sn.bbox, cropPadding);
|
|
6218
|
-
await this.replaceKind(deviceId, sn.trackId, "thumbnail", sn.timestamp, crop);
|
|
6354
|
+
thumbnailWritten = await this.replaceKind(deviceId, sn.trackId, "thumbnail", sn.timestamp, crop);
|
|
6219
6355
|
} catch (err) {
|
|
6220
6356
|
this.deps.logger.warn("event media: track thumbnail crop failed", {
|
|
6221
6357
|
tags: { deviceId },
|
|
@@ -6225,9 +6361,12 @@ var EventMediaDispatcher = class {
|
|
|
6225
6361
|
error: err instanceof Error ? err.message : String(err)
|
|
6226
6362
|
}
|
|
6227
6363
|
});
|
|
6228
|
-
if (boxed) await this.replaceKind(deviceId, sn.trackId, "thumbnail", sn.timestamp, boxed);
|
|
6364
|
+
if (boxed) thumbnailWritten = await this.replaceKind(deviceId, sn.trackId, "thumbnail", sn.timestamp, boxed);
|
|
6229
6365
|
}
|
|
6230
|
-
return
|
|
6366
|
+
return {
|
|
6367
|
+
storedSnapshot: stored,
|
|
6368
|
+
thumbnailWritten
|
|
6369
|
+
};
|
|
6231
6370
|
}
|
|
6232
6371
|
/**
|
|
6233
6372
|
* Clean subject-centered crop of `bbox` out of the raw frame — the shared
|
|
@@ -6265,6 +6404,7 @@ var EventMediaDispatcher = class {
|
|
|
6265
6404
|
timestamp,
|
|
6266
6405
|
data
|
|
6267
6406
|
});
|
|
6407
|
+
return true;
|
|
6268
6408
|
} catch (err) {
|
|
6269
6409
|
this.deps.logger.debug(`event media: ${kind} replace failed`, {
|
|
6270
6410
|
tags: { deviceId },
|
|
@@ -6274,6 +6414,7 @@ var EventMediaDispatcher = class {
|
|
|
6274
6414
|
error: err instanceof Error ? err.message : String(err)
|
|
6275
6415
|
}
|
|
6276
6416
|
});
|
|
6417
|
+
return false;
|
|
6277
6418
|
}
|
|
6278
6419
|
}
|
|
6279
6420
|
async writeEventMedia(deviceId, frameData, fw, fh, ev, cropPadding) {
|
|
@@ -7576,15 +7717,30 @@ var FaceSettingsSchema = object({
|
|
|
7576
7717
|
*/
|
|
7577
7718
|
enabled: boolean().default(true),
|
|
7578
7719
|
/** Cosine similarity (on L2-normalized arcface vectors) required to match. */
|
|
7579
|
-
similarityThreshold: number().min(0).max(1).default(.
|
|
7720
|
+
similarityThreshold: number().min(0).max(1).default(.55),
|
|
7580
7721
|
/** Reject ambiguous matches: require best − secondBest ≥ margin. */
|
|
7581
|
-
margin: number().min(0).max(1).default(.
|
|
7722
|
+
margin: number().min(0).max(1).default(.1),
|
|
7582
7723
|
/** Minimum face-detection confidence for a face to be considered. */
|
|
7583
7724
|
minFaceConfidence: number().min(0).max(1).default(.5),
|
|
7725
|
+
/**
|
|
7726
|
+
* Minimum face bbox size (px, shorter side of the face box in detection-frame
|
|
7727
|
+
* space) for a face to be eligible for embedding-based auto-matching. Below
|
|
7728
|
+
* this, ArcFace resolution is unreliable and auto-assignment produces the
|
|
7729
|
+
* observed false positives (tiny/distant faces collapsing onto one identity).
|
|
7730
|
+
* Such faces are dropped BEFORE matching/enrolment (#26.1).
|
|
7731
|
+
*/
|
|
7732
|
+
minFacePx: number().min(0).default(30),
|
|
7733
|
+
/**
|
|
7734
|
+
* Minimum enrolled-sample count an identity must have before it can be an
|
|
7735
|
+
* AUTO-MATCH target. A single-sample identity is an unreliable sink (one noisy
|
|
7736
|
+
* enrolment attracted 81% of matches); identities below this are excluded from
|
|
7737
|
+
* automatic matching until more samples are enrolled (#26.3).
|
|
7738
|
+
*/
|
|
7739
|
+
minIdentitySamples: number().int().min(1).default(2),
|
|
7584
7740
|
/** Frames an identity must be confirmed before a track is assigned. Floor of
|
|
7585
7741
|
* 1 (0 confirmations would assign on a single noisy frame — nonsensical;
|
|
7586
7742
|
* such a value falls back to the default). */
|
|
7587
|
-
confirmFrames: number().int().min(1).default(
|
|
7743
|
+
confirmFrames: number().int().min(1).default(3),
|
|
7588
7744
|
/** Recent-faces buffer retention (days). */
|
|
7589
7745
|
bufferRetentionDays: number().min(0).default(3),
|
|
7590
7746
|
/** Max buffered faces kept per device. */
|
|
@@ -7601,6 +7757,8 @@ function resolveFaceSettings(raw) {
|
|
|
7601
7757
|
similarityThreshold: pick("similarityThreshold"),
|
|
7602
7758
|
margin: pick("margin"),
|
|
7603
7759
|
minFaceConfidence: pick("minFaceConfidence"),
|
|
7760
|
+
minFacePx: pick("minFacePx"),
|
|
7761
|
+
minIdentitySamples: pick("minIdentitySamples"),
|
|
7604
7762
|
confirmFrames: pick("confirmFrames"),
|
|
7605
7763
|
bufferRetentionDays: pick("bufferRetentionDays"),
|
|
7606
7764
|
bufferMaxPerDevice: pick("bufferMaxPerDevice")
|
|
@@ -7936,10 +8094,12 @@ function evaluatePeriodicSnapshot(input) {
|
|
|
7936
8094
|
*/
|
|
7937
8095
|
function planPeriodicMedia(input) {
|
|
7938
8096
|
const appendSnapshot = input.dueSnapshot;
|
|
8097
|
+
const rollingLastFrame = input.saveThumbnails && input.now - input.lastFrameAt >= input.intervalMs && !appendSnapshot;
|
|
8098
|
+
const thumbnailLanded = input.thumbnailLanded ?? true;
|
|
7939
8099
|
return {
|
|
7940
8100
|
appendSnapshot,
|
|
7941
|
-
rollingLastFrame
|
|
7942
|
-
bestThumbnail: input.isNewBest
|
|
8101
|
+
rollingLastFrame,
|
|
8102
|
+
bestThumbnail: input.isNewBest || !thumbnailLanded
|
|
7943
8103
|
};
|
|
7944
8104
|
}
|
|
7945
8105
|
//#endregion
|
|
@@ -8922,6 +9082,21 @@ var ObjectEmbeddingStore = class {
|
|
|
8922
9082
|
//#endregion
|
|
8923
9083
|
//#region src/pipeline-analytics/pipeline/face-matcher.ts
|
|
8924
9084
|
/**
|
|
9085
|
+
* Count enrolled samples per identity for probes of a matching model+dimension.
|
|
9086
|
+
* Only identities meeting `minIdentitySamples` are eligible auto-match targets.
|
|
9087
|
+
*/
|
|
9088
|
+
function eligibleIdentities(gallery, probeModelId, probeDim, minIdentitySamples) {
|
|
9089
|
+
const counts = /* @__PURE__ */ new Map();
|
|
9090
|
+
for (const s of gallery) {
|
|
9091
|
+
if (s.modelId !== probeModelId) continue;
|
|
9092
|
+
if (s.embedding.length !== probeDim) continue;
|
|
9093
|
+
counts.set(s.identityId, (counts.get(s.identityId) ?? 0) + 1);
|
|
9094
|
+
}
|
|
9095
|
+
const eligible = /* @__PURE__ */ new Set();
|
|
9096
|
+
for (const [id, count] of counts) if (count >= minIdentitySamples) eligible.add(id);
|
|
9097
|
+
return eligible;
|
|
9098
|
+
}
|
|
9099
|
+
/**
|
|
8925
9100
|
* Assign at most one identity per track AND at most one track per identity for
|
|
8926
9101
|
* a single frame. Greedy by score: compute every candidate's full ranked match
|
|
8927
9102
|
* list, then repeatedly take the globally-highest (track, identity) pair whose
|
|
@@ -8932,10 +9107,12 @@ function assignUniquePerFrame(candidates, gallery, opts) {
|
|
|
8932
9107
|
const pairs = [];
|
|
8933
9108
|
candidates.forEach((c, trackIdx) => {
|
|
8934
9109
|
const probeVec = new Float32Array(c.embedding);
|
|
9110
|
+
const eligible = eligibleIdentities(gallery, c.modelId, c.embedding.length, opts.minIdentitySamples ?? 1);
|
|
8935
9111
|
const bestByIdentity = /* @__PURE__ */ new Map();
|
|
8936
9112
|
for (const s of gallery) {
|
|
8937
9113
|
if (s.modelId !== c.modelId) continue;
|
|
8938
9114
|
if (s.embedding.length !== c.embedding.length) continue;
|
|
9115
|
+
if (!eligible.has(s.identityId)) continue;
|
|
8939
9116
|
const score = cosineSimilarity(probeVec, new Float32Array(s.embedding));
|
|
8940
9117
|
const prev = bestByIdentity.get(s.identityId);
|
|
8941
9118
|
if (prev === void 0 || score > prev) bestByIdentity.set(s.identityId, score);
|
|
@@ -9081,7 +9258,7 @@ var FaceRecognizer = class {
|
|
|
9081
9258
|
}
|
|
9082
9259
|
async processFrame(input) {
|
|
9083
9260
|
const { settings } = input;
|
|
9084
|
-
const candidates = input.tracked.filter((t) => Array.isArray(t.embedding) && t.embedding.length > 0 && typeof t.embeddingModelId === "string" && t.confidence >= settings.minFaceConfidence);
|
|
9261
|
+
const candidates = input.tracked.filter((t) => Array.isArray(t.embedding) && t.embedding.length > 0 && typeof t.embeddingModelId === "string" && t.confidence >= settings.minFaceConfidence && (t.faceBbox === void 0 || Math.min(t.faceBbox.w, t.faceBbox.h) >= settings.minFacePx));
|
|
9085
9262
|
if (candidates.length === 0) return;
|
|
9086
9263
|
this.deps.logger.debug("face: frame candidates", {
|
|
9087
9264
|
tags: { deviceId: input.deviceId },
|
|
@@ -9097,7 +9274,8 @@ var FaceRecognizer = class {
|
|
|
9097
9274
|
modelId: c.embeddingModelId
|
|
9098
9275
|
})), this.gallery, {
|
|
9099
9276
|
threshold: settings.similarityThreshold,
|
|
9100
|
-
margin: settings.margin
|
|
9277
|
+
margin: settings.margin,
|
|
9278
|
+
minIdentitySamples: settings.minIdentitySamples
|
|
9101
9279
|
}) : /* @__PURE__ */ new Map();
|
|
9102
9280
|
const labelWork = [];
|
|
9103
9281
|
for (const c of candidates) {
|
|
@@ -11344,6 +11522,12 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
11344
11522
|
faceGlobalEnabledCache = null;
|
|
11345
11523
|
mediaCacheByDevice = /* @__PURE__ */ new Map();
|
|
11346
11524
|
packageDropCacheByDevice = /* @__PURE__ */ new Map();
|
|
11525
|
+
/** Per-device stationary settings cache (#31), TTL-mirrored like media/tracking.
|
|
11526
|
+
* The registry is a single shared instance, so per-(device) suppress/wake IoU,
|
|
11527
|
+
* promotion window, TTL and the master toggle are resolved from this cache at
|
|
11528
|
+
* the partition + promotion + sweep call sites — an operator change takes
|
|
11529
|
+
* effect within one SETTINGS_CACHE_TTL_MS tick without an addon restart. */
|
|
11530
|
+
stationaryCacheByDevice = /* @__PURE__ */ new Map();
|
|
11347
11531
|
/** Turns stationary appear/depart into package-delivered/picked-up events. */
|
|
11348
11532
|
packageDropDetector = null;
|
|
11349
11533
|
/** Consecutive detector-dropout frames skipped per (deviceId, source) key. */
|
|
@@ -11376,6 +11560,12 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
11376
11560
|
* where no `snapshot` is appended, so it is never byte-identical to a stored
|
|
11377
11561
|
* `snapshot` (kills the end-of-track duplicate). Cleared on track end. */
|
|
11378
11562
|
lastFrameAtByTrack = /* @__PURE__ */ new Map();
|
|
11563
|
+
/** Track ids whose best `thumbnail` has ACTUALLY been persisted (#27-A). A
|
|
11564
|
+
* track absent here keeps forcing a best-thumbnail capture every frame until
|
|
11565
|
+
* one lands, so a short / high-churn track whose first capture was dropped
|
|
11566
|
+
* (recycled/blank live frame) still gets a subject crop for the gallery
|
|
11567
|
+
* instead of degrading to a full-scene tile. Cleared on track end + reset. */
|
|
11568
|
+
thumbnailLandedTracks = /* @__PURE__ */ new Set();
|
|
11379
11569
|
/** Per-track update-gate memory for `PipelineAnalyticsTrackLifecycle`
|
|
11380
11570
|
* `phase:'update'` — the last-emitted best (confidence / label / crop
|
|
11381
11571
|
* area) + emit time, so a material improvement is measured against the
|
|
@@ -11447,6 +11637,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
11447
11637
|
this.stationaryRegistry = new StationaryObjectRegistry({
|
|
11448
11638
|
store: api.settingsStore,
|
|
11449
11639
|
logger: logger.child("StationaryRegistry"),
|
|
11640
|
+
ttlForDevice: (deviceId) => this.stationarySettingsFromCache(deviceId).entryTtlMs,
|
|
11450
11641
|
onChange: ({ phase, entry, timestamp }) => {
|
|
11451
11642
|
this.ctx.eventBus.emit({
|
|
11452
11643
|
id: `pa-stationary-${entry.id}-${phase}`,
|
|
@@ -11822,6 +12013,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
11822
12013
|
this.faceCacheByDevice.delete(data.deviceId);
|
|
11823
12014
|
this.mediaCacheByDevice.delete(data.deviceId);
|
|
11824
12015
|
this.packageDropCacheByDevice.delete(data.deviceId);
|
|
12016
|
+
this.stationaryCacheByDevice.delete(data.deviceId);
|
|
11825
12017
|
}
|
|
11826
12018
|
});
|
|
11827
12019
|
this.unsubDeviceUnreg = this.ctx.eventBus.subscribe({ category: EventCategory.DeviceUnregistered }, (ev) => {
|
|
@@ -11838,6 +12030,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
11838
12030
|
this.faceCacheByDevice.delete(deviceId);
|
|
11839
12031
|
this.mediaCacheByDevice.delete(deviceId);
|
|
11840
12032
|
this.packageDropCacheByDevice.delete(deviceId);
|
|
12033
|
+
this.stationaryCacheByDevice.delete(deviceId);
|
|
11841
12034
|
this.bindingCache?.invalidate(deviceId);
|
|
11842
12035
|
this.zoneAnalytics?.forgetDevice(deviceId);
|
|
11843
12036
|
this.audioMetrics?.forgetDevice(deviceId);
|
|
@@ -12176,6 +12369,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12176
12369
|
this.dropoutSkipsByKey.clear();
|
|
12177
12370
|
this.bestFrameTracker.clear();
|
|
12178
12371
|
this.lastFrameAtByTrack.clear();
|
|
12372
|
+
this.thumbnailLandedTracks.clear();
|
|
12179
12373
|
this.trackLifecycleUpdateMem.clear();
|
|
12180
12374
|
this.objectEmbeddingBestSelector.clear();
|
|
12181
12375
|
this.levelStateByDevice.clear();
|
|
@@ -12186,6 +12380,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12186
12380
|
this.faceGlobalEnabledCache = null;
|
|
12187
12381
|
this.mediaCacheByDevice.clear();
|
|
12188
12382
|
this.packageDropCacheByDevice.clear();
|
|
12383
|
+
this.stationaryCacheByDevice.clear();
|
|
12189
12384
|
this.trackStore?.clearAll();
|
|
12190
12385
|
this.stationaryRegistry = null;
|
|
12191
12386
|
this.bindingCache?.clearAll();
|
|
@@ -12230,6 +12425,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12230
12425
|
return;
|
|
12231
12426
|
}
|
|
12232
12427
|
this.dropoutSkipsByKey.set(key, 0);
|
|
12428
|
+
if (source === "pipeline" && this.stationaryRegistry) await this.resolveDeviceStationarySettings(deviceId);
|
|
12233
12429
|
const processor = await this.getOrCreateProcessor(deviceId, source);
|
|
12234
12430
|
const proxy = await this.ensureProxy(deviceId);
|
|
12235
12431
|
const liveZones = proxy?.state.zones.value?.zones ?? [];
|
|
@@ -12369,10 +12565,15 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12369
12565
|
} });
|
|
12370
12566
|
}
|
|
12371
12567
|
this.lastActiveTrackIds.set(key, currentTrackIds);
|
|
12372
|
-
|
|
12568
|
+
const stationarySettings = this.stationarySettingsFromCache(deviceId);
|
|
12569
|
+
if (source === "pipeline" && this.stationaryRegistry && stationarySettings.enabled) {
|
|
12373
12570
|
const dims = this.lastFrameDimsByDevice.get(deviceId);
|
|
12374
12571
|
if (dims && dims.w > 0 && dims.h > 0) {
|
|
12375
12572
|
const refDiag = Math.hypot(dims.w, dims.h);
|
|
12573
|
+
const promotionConfig = {
|
|
12574
|
+
...DEFAULT_PROMOTION_CONFIG,
|
|
12575
|
+
windowMs: stationarySettings.promotionWindowMs
|
|
12576
|
+
};
|
|
12376
12577
|
for (const t of result.tracked) {
|
|
12377
12578
|
const active = this.trackStore.peekActive(t.trackId);
|
|
12378
12579
|
if (!active) continue;
|
|
@@ -12380,7 +12581,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12380
12581
|
positions: active.positions,
|
|
12381
12582
|
referenceDiagonalPx: refDiag,
|
|
12382
12583
|
now: result.timestamp,
|
|
12383
|
-
config:
|
|
12584
|
+
config: promotionConfig
|
|
12384
12585
|
});
|
|
12385
12586
|
if (!promote) continue;
|
|
12386
12587
|
this.promoteToStationary({
|
|
@@ -12507,6 +12708,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12507
12708
|
},
|
|
12508
12709
|
mediaKey: s.mediaKey
|
|
12509
12710
|
});
|
|
12711
|
+
for (const trackId of res.thumbnailTrackIds) this.thumbnailLandedTracks.add(trackId);
|
|
12510
12712
|
}).catch(() => {});
|
|
12511
12713
|
}
|
|
12512
12714
|
}
|
|
@@ -12661,6 +12863,20 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12661
12863
|
});
|
|
12662
12864
|
return settings;
|
|
12663
12865
|
}
|
|
12866
|
+
async resolveDeviceStationarySettings(deviceId) {
|
|
12867
|
+
const now = Date.now();
|
|
12868
|
+
const cached = this.stationaryCacheByDevice.get(deviceId);
|
|
12869
|
+
if (cached && now < cached.expiresAt) return cached.settings;
|
|
12870
|
+
const settings = resolveStationarySettings(await this.ctx?.settings?.readDeviceStore(deviceId) ?? {});
|
|
12871
|
+
this.stationaryCacheByDevice.set(deviceId, {
|
|
12872
|
+
settings,
|
|
12873
|
+
expiresAt: now + SETTINGS_CACHE_TTL_MS
|
|
12874
|
+
});
|
|
12875
|
+
return settings;
|
|
12876
|
+
}
|
|
12877
|
+
stationarySettingsFromCache(deviceId) {
|
|
12878
|
+
return this.stationaryCacheByDevice.get(deviceId)?.settings ?? STATIONARY_DEFAULTS;
|
|
12879
|
+
}
|
|
12664
12880
|
async resolveDevicePackageDropSettings(deviceId) {
|
|
12665
12881
|
const now = Date.now();
|
|
12666
12882
|
const cached = this.packageDropCacheByDevice.get(deviceId);
|
|
@@ -12758,6 +12974,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12758
12974
|
if (!this.faceRecognizer || detail.embedding === void 0) return;
|
|
12759
12975
|
if (!await this.resolveGlobalFaceEnabled()) return;
|
|
12760
12976
|
const [settings, media] = await Promise.all([this.resolveDeviceFaceSettings(deviceId), this.resolveDeviceMediaSettings(deviceId)]);
|
|
12977
|
+
if (detail.bbox !== void 0 && Math.min(detail.bbox.w, detail.bbox.h) < settings.minFacePx) return;
|
|
12761
12978
|
await this.faceRecognizer.ingestFaceDetail({
|
|
12762
12979
|
deviceId,
|
|
12763
12980
|
trackId,
|
|
@@ -13036,12 +13253,14 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13036
13253
|
frameWidth,
|
|
13037
13254
|
frameHeight
|
|
13038
13255
|
});
|
|
13039
|
-
const
|
|
13256
|
+
const centerScore = bboxCenterScore(t.bbox, frameWidth, frameHeight);
|
|
13257
|
+
const isNewBest = this.bestFrameTracker.observe(t.trackId, t.confidence, timestamp, edgeClear, centerScore);
|
|
13040
13258
|
this.maybeEmitTrackLifecycleUpdate(deviceId, t, timestamp, isNewBest);
|
|
13041
13259
|
const plan = planPeriodicMedia({
|
|
13042
13260
|
saveThumbnails: media.saveThumbnails,
|
|
13043
13261
|
dueSnapshot,
|
|
13044
13262
|
isNewBest,
|
|
13263
|
+
thumbnailLanded: this.thumbnailLandedTracks.has(t.trackId),
|
|
13045
13264
|
lastFrameAt: this.lastFrameAtByTrack.get(t.trackId) ?? 0,
|
|
13046
13265
|
now: timestamp,
|
|
13047
13266
|
intervalMs: media.snapshotIntervalMs
|
|
@@ -13362,6 +13581,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13362
13581
|
this.bestFrameTracker.delete(t.trackId);
|
|
13363
13582
|
this.objectEmbeddingBestSelector.delete(t.trackId);
|
|
13364
13583
|
this.lastFrameAtByTrack.delete(t.trackId);
|
|
13584
|
+
this.thumbnailLandedTracks.delete(t.trackId);
|
|
13365
13585
|
this.detailDispatcher?.onTrackEnded(t.deviceId, t.trackId);
|
|
13366
13586
|
this.overlayState.onTrackEnded(t.deviceId, t.trackId);
|
|
13367
13587
|
if ((this.trackStore?.getActive(t.deviceId).length ?? 0) === 0) {
|
|
@@ -13623,6 +13843,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13623
13843
|
this.bestFrameTracker.delete(track.trackId);
|
|
13624
13844
|
this.objectEmbeddingBestSelector.delete(track.trackId);
|
|
13625
13845
|
this.lastFrameAtByTrack.delete(track.trackId);
|
|
13846
|
+
this.thumbnailLandedTracks.delete(track.trackId);
|
|
13626
13847
|
this.trackLifecycleUpdateMem.delete(track.trackId);
|
|
13627
13848
|
this.detailDispatcher?.onTrackEnded(deviceId, track.trackId);
|
|
13628
13849
|
this.overlayState.onTrackEnded(deviceId, track.trackId);
|
|
@@ -13664,15 +13885,27 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13664
13885
|
}, source);
|
|
13665
13886
|
if (source === "pipeline" && this.stationaryRegistry) {
|
|
13666
13887
|
const registry = this.stationaryRegistry;
|
|
13667
|
-
p.setStationaryGate({ filter: (input) =>
|
|
13668
|
-
deviceId
|
|
13669
|
-
|
|
13670
|
-
|
|
13671
|
-
|
|
13672
|
-
|
|
13673
|
-
|
|
13674
|
-
|
|
13675
|
-
|
|
13888
|
+
p.setStationaryGate({ filter: (input) => {
|
|
13889
|
+
const s = this.stationarySettingsFromCache(deviceId);
|
|
13890
|
+
if (!s.enabled) return {
|
|
13891
|
+
suppressedIndices: /* @__PURE__ */ new Set(),
|
|
13892
|
+
confirmed: [],
|
|
13893
|
+
wokenEntryIds: []
|
|
13894
|
+
};
|
|
13895
|
+
const matchConfig = {
|
|
13896
|
+
suppressIou: s.suppressIou,
|
|
13897
|
+
wakeAssocIou: s.wakeAssocIou
|
|
13898
|
+
};
|
|
13899
|
+
return registry.filter({
|
|
13900
|
+
deviceId,
|
|
13901
|
+
detections: input.detections.map((d) => ({
|
|
13902
|
+
bbox: d.bbox,
|
|
13903
|
+
className: d.class
|
|
13904
|
+
})),
|
|
13905
|
+
frameWidth: input.frameWidth,
|
|
13906
|
+
frameHeight: input.frameHeight
|
|
13907
|
+
}, matchConfig);
|
|
13908
|
+
} });
|
|
13676
13909
|
}
|
|
13677
13910
|
this.processors.set(key, p);
|
|
13678
13911
|
}
|
|
@@ -14735,6 +14968,25 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
14735
14968
|
step: .05,
|
|
14736
14969
|
default: FACE_DEFAULTS.minFaceConfidence
|
|
14737
14970
|
},
|
|
14971
|
+
{
|
|
14972
|
+
type: "number",
|
|
14973
|
+
key: "minFacePx",
|
|
14974
|
+
label: "Min face size",
|
|
14975
|
+
description: "Minimum face box size (shorter side, pixels) for a face to be eligible for auto-matching. Faces smaller than this are too low-resolution for reliable recognition and are ignored. Raise it to suppress false matches on tiny/distant faces.",
|
|
14976
|
+
min: 0,
|
|
14977
|
+
step: 1,
|
|
14978
|
+
default: FACE_DEFAULTS.minFacePx,
|
|
14979
|
+
unit: "px"
|
|
14980
|
+
},
|
|
14981
|
+
{
|
|
14982
|
+
type: "number",
|
|
14983
|
+
key: "minIdentitySamples",
|
|
14984
|
+
label: "Min identity samples",
|
|
14985
|
+
description: "Minimum enrolled sample count before an identity can be an automatic match target. Identities with fewer samples are ignored by auto-matching (prevents a single unreliable sample from attracting many faces).",
|
|
14986
|
+
min: 1,
|
|
14987
|
+
step: 1,
|
|
14988
|
+
default: FACE_DEFAULTS.minIdentitySamples
|
|
14989
|
+
},
|
|
14738
14990
|
{
|
|
14739
14991
|
type: "number",
|
|
14740
14992
|
key: "confirmFrames",
|
|
@@ -14945,6 +15197,70 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
14945
15197
|
default: TRACKING_DEFAULTS.dropoutMaxSkipFrames
|
|
14946
15198
|
}
|
|
14947
15199
|
]
|
|
15200
|
+
},
|
|
15201
|
+
{
|
|
15202
|
+
id: "stationary-objects",
|
|
15203
|
+
title: "Stationary objects",
|
|
15204
|
+
tab: "analytics",
|
|
15205
|
+
description: "Parked / idle object suppression — keeps a stopped car from re-spawning tracks and re-flooding events. Per-device overrides on each camera. Defaults match today’s behaviour.",
|
|
15206
|
+
columns: 2,
|
|
15207
|
+
fields: [
|
|
15208
|
+
{
|
|
15209
|
+
type: "boolean",
|
|
15210
|
+
key: "enabled",
|
|
15211
|
+
label: "Suppress parked objects",
|
|
15212
|
+
description: "Promote a stopped object to a lightweight registry entry and suppress its detections from re-spawning tracks. Off = every parked object keeps spawning normal tracks on this camera.",
|
|
15213
|
+
default: STATIONARY_DEFAULTS.enabled
|
|
15214
|
+
},
|
|
15215
|
+
{
|
|
15216
|
+
type: "slider",
|
|
15217
|
+
key: "suppressIou",
|
|
15218
|
+
label: "Suppress IoU",
|
|
15219
|
+
description: "Overlap at/above which a detection is the SAME parked object (unmoved) → its spawn is suppressed. Lower can suppress genuine new tracks near a parked object.",
|
|
15220
|
+
min: .3,
|
|
15221
|
+
max: .9,
|
|
15222
|
+
step: .05,
|
|
15223
|
+
default: STATIONARY_DEFAULTS.suppressIou,
|
|
15224
|
+
showValue: true
|
|
15225
|
+
},
|
|
15226
|
+
{
|
|
15227
|
+
type: "slider",
|
|
15228
|
+
key: "wakeAssocIou",
|
|
15229
|
+
label: "Wake / associate IoU",
|
|
15230
|
+
description: "Minimum overlap for a detection to be treated as a parked entry’s OWN object (confirm or wake it). Too low lets a passing object wake a parked entry (re-flood); too high can miss the real departure.",
|
|
15231
|
+
min: .02,
|
|
15232
|
+
max: .5,
|
|
15233
|
+
step: .02,
|
|
15234
|
+
default: STATIONARY_DEFAULTS.wakeAssocIou,
|
|
15235
|
+
showValue: true
|
|
15236
|
+
},
|
|
15237
|
+
{
|
|
15238
|
+
type: "slider",
|
|
15239
|
+
key: "promotionWindowMs",
|
|
15240
|
+
label: "Promotion stillness window",
|
|
15241
|
+
description: "How long (ms) an object must stay put — and the track must have existed — before it is promoted to a parked entry. Longer = slower to stop a re-spawn flood.",
|
|
15242
|
+
min: 5e3,
|
|
15243
|
+
max: 12e4,
|
|
15244
|
+
step: 1e3,
|
|
15245
|
+
default: STATIONARY_DEFAULTS.promotionWindowMs,
|
|
15246
|
+
showValue: true,
|
|
15247
|
+
unit: "s",
|
|
15248
|
+
displayScale: 1e3
|
|
15249
|
+
},
|
|
15250
|
+
{
|
|
15251
|
+
type: "slider",
|
|
15252
|
+
key: "entryTtlMs",
|
|
15253
|
+
label: "Observed-time TTL",
|
|
15254
|
+
description: "Retire a parked entry after this much OBSERVED (frames-flowing) time without a confirming detection — the object was removed while watched, or a long occlusion.",
|
|
15255
|
+
min: 6e4,
|
|
15256
|
+
max: 18e5,
|
|
15257
|
+
step: 3e4,
|
|
15258
|
+
default: STATIONARY_DEFAULTS.entryTtlMs,
|
|
15259
|
+
showValue: true,
|
|
15260
|
+
unit: "s",
|
|
15261
|
+
displayScale: 1e3
|
|
15262
|
+
}
|
|
15263
|
+
]
|
|
14948
15264
|
}
|
|
14949
15265
|
] });
|
|
14950
15266
|
}
|