@camstack/addon-post-analysis 1.1.25 → 1.1.27
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-Csk_yJr_.mjs → dist-BqOBYSWs.mjs} +181 -8
- package/dist/{dist-CCC79h7t.js → dist-CtnFKuWh.js} +186 -7
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/enrichment-engine/index.js +2 -2
- package/dist/enrichment-engine/index.mjs +1 -1
- package/dist/{node-CIEkEv1F.js → node-BN31iDiA.js} +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Cb6Z2H5K.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Dvz_QBOD.mjs} +3 -3
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DtOrjE2U.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BXQHc7Tq.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-BrqCuPYQ.mjs → hostInit-C7nUOwbi.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +1348 -43
- package/dist/pipeline-analytics/index.mjs +1346 -41
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/dist/{resolve-frame-sKYbstL-.js → resolve-frame-BAdpVnlx.js} +1 -1
- package/package.json +1 -1
|
@@ -2,8 +2,8 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
6
|
-
const require_resolve_frame = require("../resolve-frame-
|
|
5
|
+
const require_dist = require("../dist-CtnFKuWh.js");
|
|
6
|
+
const require_resolve_frame = require("../resolve-frame-BAdpVnlx.js");
|
|
7
7
|
let _camstack_shm_ring = require("@camstack/shm-ring");
|
|
8
8
|
let sharp = require("sharp");
|
|
9
9
|
sharp = require_dist.__toESM(sharp);
|
|
@@ -124,6 +124,138 @@ function clusterByEmbedding(items, threshold = .5) {
|
|
|
124
124
|
})).toSorted((a, b) => b.size - a.size || b.cohesion - a.cohesion);
|
|
125
125
|
}
|
|
126
126
|
//#endregion
|
|
127
|
+
//#region src/pipeline-analytics/pipeline/key-event-scoring.ts
|
|
128
|
+
/**
|
|
129
|
+
* key-event-scoring — the SINGLE deterministic "importance" scorer for a
|
|
130
|
+
* completed track. A value in [0,1] the viewer sorts events by (events page)
|
|
131
|
+
* and a future reels feature pulls highlights from.
|
|
132
|
+
*
|
|
133
|
+
* PURE + DETERMINISTIC: no Date.now / no random. Time enters ONLY as the
|
|
134
|
+
* precomputed `durationMs` (the caller already knows a track's dwell). Given
|
|
135
|
+
* the same `ScoringInput`, `computeImportance` always returns the same
|
|
136
|
+
* `{ importance, reason }`.
|
|
137
|
+
*
|
|
138
|
+
* The score is a weighted sum of seven independent, individually-saturated
|
|
139
|
+
* signals. Every weight and knee is a NAMED EXPORT so the formula is auditable
|
|
140
|
+
* and the tests can assert exact wiring. The weights sum to exactly 1.0, so an
|
|
141
|
+
* all-signals-maxed track scores exactly 1.0 (the clamp is then a no-op) and an
|
|
142
|
+
* all-zero track scores 0.
|
|
143
|
+
*/
|
|
144
|
+
var WEIGHT_CONFIDENCE = .3;
|
|
145
|
+
var WEIGHT_CLASS = .2;
|
|
146
|
+
var WEIGHT_DWELL = .2;
|
|
147
|
+
var WEIGHT_PROXIMITY = .12;
|
|
148
|
+
var WEIGHT_TRAVEL = .08;
|
|
149
|
+
var WEIGHT_ZONE = .05;
|
|
150
|
+
var WEIGHT_IDENTITY = .05;
|
|
151
|
+
/** Dwell (ms) that saturates the dwell term. */
|
|
152
|
+
var DWELL_FULL_MS = 6e4;
|
|
153
|
+
/** Peak bbox area (as a fraction of frame area) that saturates the size term. */
|
|
154
|
+
var SIZE_FULL = .15;
|
|
155
|
+
var CLASS_RANK_VEHICLE = .8;
|
|
156
|
+
var CLASS_RANK_ANIMAL = .5;
|
|
157
|
+
var CLASS_RANK_DEFAULT = .25;
|
|
158
|
+
var PERSON_CLASSES = new Set(["person", "face"]);
|
|
159
|
+
var VEHICLE_CLASSES = new Set([
|
|
160
|
+
"vehicle",
|
|
161
|
+
"car",
|
|
162
|
+
"truck",
|
|
163
|
+
"bus"
|
|
164
|
+
]);
|
|
165
|
+
var ANIMAL_CLASSES = new Set([
|
|
166
|
+
"animal",
|
|
167
|
+
"dog",
|
|
168
|
+
"cat"
|
|
169
|
+
]);
|
|
170
|
+
/** Clamp to [0,1]. */
|
|
171
|
+
function clamp01(x) {
|
|
172
|
+
if (Number.isNaN(x)) return 0;
|
|
173
|
+
if (x < 0) return 0;
|
|
174
|
+
if (x > 1) return 1;
|
|
175
|
+
return x;
|
|
176
|
+
}
|
|
177
|
+
/** Rank a class name into [0,1] by how event-worthy it is. Case-insensitive. */
|
|
178
|
+
function classRank(className) {
|
|
179
|
+
const c = className.toLowerCase();
|
|
180
|
+
if (PERSON_CLASSES.has(c)) return 1;
|
|
181
|
+
if (VEHICLE_CLASSES.has(c)) return CLASS_RANK_VEHICLE;
|
|
182
|
+
if (ANIMAL_CLASSES.has(c)) return CLASS_RANK_ANIMAL;
|
|
183
|
+
return CLASS_RANK_DEFAULT;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Compute a deterministic importance score for a completed track.
|
|
187
|
+
*
|
|
188
|
+
* `reason` is the tag of the single largest weighted term. Ties resolve to the
|
|
189
|
+
* first term in a fixed, weight-descending order (confidence → class → dwell →
|
|
190
|
+
* proximity → travel → zone → identity), so the output stays deterministic.
|
|
191
|
+
*/
|
|
192
|
+
function computeImportance(input) {
|
|
193
|
+
const terms = [
|
|
194
|
+
{
|
|
195
|
+
reason: "confidence",
|
|
196
|
+
value: WEIGHT_CONFIDENCE * clamp01(input.peakConfidence)
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
reason: "class",
|
|
200
|
+
value: WEIGHT_CLASS * classRank(input.className)
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
reason: "dwell",
|
|
204
|
+
value: WEIGHT_DWELL * clamp01(input.durationMs / DWELL_FULL_MS)
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
reason: "proximity",
|
|
208
|
+
value: WEIGHT_PROXIMITY * clamp01(input.peakBboxAreaFrac / SIZE_FULL)
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
reason: "travel",
|
|
212
|
+
value: WEIGHT_TRAVEL * clamp01(input.totalDistance / 2)
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
reason: "zone",
|
|
216
|
+
value: WEIGHT_ZONE * (input.zonesVisited.length > 0 ? 1 : 0)
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
reason: "identity",
|
|
220
|
+
value: WEIGHT_IDENTITY * (input.label ? 1 : 0)
|
|
221
|
+
}
|
|
222
|
+
];
|
|
223
|
+
let sum = 0;
|
|
224
|
+
let best = terms[0];
|
|
225
|
+
for (const term of terms) {
|
|
226
|
+
sum += term.value;
|
|
227
|
+
if (term.value > best.value) best = term;
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
importance: clamp01(sum),
|
|
231
|
+
reason: best.reason
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region src/pipeline-analytics/pipeline/key-event-recompute.ts
|
|
236
|
+
/**
|
|
237
|
+
* Recompute + persist a persisted track's importance from its current fields
|
|
238
|
+
* (including any newly-assigned label) and re-stamp its object events. No-op
|
|
239
|
+
* when the track is not (yet) persisted.
|
|
240
|
+
*/
|
|
241
|
+
async function recomputeTrackImportance(deps, trackId) {
|
|
242
|
+
const track = await deps.trackStore.getPersistedByTrackId(trackId);
|
|
243
|
+
if (track === null) return;
|
|
244
|
+
const peak = await deps.eventStore.peakForTrack(trackId);
|
|
245
|
+
const { importance, reason } = computeImportance({
|
|
246
|
+
peakConfidence: peak.peakConfidence,
|
|
247
|
+
className: track.className,
|
|
248
|
+
durationMs: track.lastSeen - track.firstSeen,
|
|
249
|
+
peakBboxAreaFrac: peak.peakBboxAreaFrac,
|
|
250
|
+
totalDistance: track.totalDistance,
|
|
251
|
+
zonesVisited: track.zonesVisited,
|
|
252
|
+
...track.label !== void 0 ? { label: track.label } : {}
|
|
253
|
+
});
|
|
254
|
+
const bestEventId = track.bestEventId ?? peak.bestEventId;
|
|
255
|
+
await deps.trackStore.setImportance(trackId, importance, reason, bestEventId);
|
|
256
|
+
await deps.eventStore.setImportanceForTrack(trackId, importance);
|
|
257
|
+
}
|
|
258
|
+
//#endregion
|
|
127
259
|
//#region src/pipeline-analytics/face-gallery-provider.ts
|
|
128
260
|
/** Embedding model id used when enrolling face crops as identity samples. */
|
|
129
261
|
var MODEL_ID = "arcface-r100";
|
|
@@ -244,7 +376,9 @@ var FaceGalleryProvider = class {
|
|
|
244
376
|
...face.recognizedIdentityId != null ? { recognizedIdentityId: face.recognizedIdentityId } : {},
|
|
245
377
|
...identityName !== void 0 ? { identityName } : {},
|
|
246
378
|
assigned: face.assigned,
|
|
247
|
-
...base64 !== void 0 ? { base64 } : {}
|
|
379
|
+
...base64 !== void 0 ? { base64 } : {},
|
|
380
|
+
...face.faceBbox !== void 0 ? { faceBbox: face.faceBbox } : {},
|
|
381
|
+
...face.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: face.keyFrameMediaKey } : {}
|
|
248
382
|
});
|
|
249
383
|
}
|
|
250
384
|
return result;
|
|
@@ -272,7 +406,9 @@ var FaceGalleryProvider = class {
|
|
|
272
406
|
...face.recognizedIdentityId != null ? { recognizedIdentityId: face.recognizedIdentityId } : {},
|
|
273
407
|
...identityName !== void 0 ? { identityName } : {},
|
|
274
408
|
assigned: face.assigned,
|
|
275
|
-
...base64 !== void 0 ? { base64 } : {}
|
|
409
|
+
...base64 !== void 0 ? { base64 } : {},
|
|
410
|
+
...face.faceBbox !== void 0 ? { faceBbox: face.faceBbox } : {},
|
|
411
|
+
...face.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: face.keyFrameMediaKey } : {}
|
|
276
412
|
};
|
|
277
413
|
}
|
|
278
414
|
/**
|
|
@@ -349,6 +485,10 @@ var FaceGalleryProvider = class {
|
|
|
349
485
|
if (identityName !== void 0) {
|
|
350
486
|
await this.trackStore.setLabel(currentFace.trackId, identityName);
|
|
351
487
|
await this.eventStore.setLabelForTrack(currentFace.trackId, identityName);
|
|
488
|
+
await recomputeTrackImportance({
|
|
489
|
+
trackStore: this.trackStore,
|
|
490
|
+
eventStore: this.eventStore
|
|
491
|
+
}, currentFace.trackId);
|
|
352
492
|
}
|
|
353
493
|
this.refreshGallery();
|
|
354
494
|
}
|
|
@@ -1387,6 +1527,7 @@ var FrameProcessor = class {
|
|
|
1387
1527
|
const embeddingByBbox = /* @__PURE__ */ new Map();
|
|
1388
1528
|
const firstLevelBboxById = /* @__PURE__ */ new Map();
|
|
1389
1529
|
const faceBboxByBbox = /* @__PURE__ */ new Map();
|
|
1530
|
+
const faceAlignedCropByBbox = /* @__PURE__ */ new Map();
|
|
1390
1531
|
const plateByBbox = /* @__PURE__ */ new Map();
|
|
1391
1532
|
const maskByBbox = /* @__PURE__ */ new Map();
|
|
1392
1533
|
const flatDetections = frame.detections.filter((d) => d.kind === "first-level").map((det) => {
|
|
@@ -1429,6 +1570,11 @@ var FrameProcessor = class {
|
|
|
1429
1570
|
w: det.bbox.width,
|
|
1430
1571
|
h: det.bbox.height
|
|
1431
1572
|
});
|
|
1573
|
+
if (det.faceAlignedCrop !== void 0) faceAlignedCropByBbox.set(parentBbox, det.faceAlignedCrop);
|
|
1574
|
+
if (det.embedding !== void 0 && !embeddingByBbox.has(parentBbox)) embeddingByBbox.set(parentBbox, {
|
|
1575
|
+
embedding: det.embedding,
|
|
1576
|
+
...det.embeddingModelId !== void 0 ? { embeddingModelId: det.embeddingModelId } : {}
|
|
1577
|
+
});
|
|
1432
1578
|
}
|
|
1433
1579
|
for (const det of frame.detections) {
|
|
1434
1580
|
if (det.kind !== "detail" || det.macroClass !== "plate" || !det.parentId) continue;
|
|
@@ -1467,6 +1613,7 @@ var FrameProcessor = class {
|
|
|
1467
1613
|
});
|
|
1468
1614
|
const emb = embeddingByBbox.get(td.bbox);
|
|
1469
1615
|
const faceBbox = faceBboxByBbox.get(td.bbox);
|
|
1616
|
+
const faceAlignedCrop = faceAlignedCropByBbox.get(td.bbox);
|
|
1470
1617
|
const plate = plateByBbox.get(td.bbox);
|
|
1471
1618
|
return {
|
|
1472
1619
|
trackId: td.trackId,
|
|
@@ -1481,6 +1628,7 @@ var FrameProcessor = class {
|
|
|
1481
1628
|
...emb.embeddingModelId !== void 0 ? { embeddingModelId: emb.embeddingModelId } : {}
|
|
1482
1629
|
} : {},
|
|
1483
1630
|
...faceBbox !== void 0 ? { faceBbox } : {},
|
|
1631
|
+
...faceAlignedCrop !== void 0 ? { faceAlignedCrop } : {},
|
|
1484
1632
|
...plate !== void 0 ? {
|
|
1485
1633
|
plateText: plate.text,
|
|
1486
1634
|
plateScore: plate.score,
|
|
@@ -1569,6 +1717,115 @@ var BestDetectionTracker = class {
|
|
|
1569
1717
|
}
|
|
1570
1718
|
};
|
|
1571
1719
|
//#endregion
|
|
1720
|
+
//#region src/pipeline-analytics/pipeline/track-best-detection.ts
|
|
1721
|
+
/**
|
|
1722
|
+
* `TrackBestSelector` — the ONE unified "best detection per track" primitive.
|
|
1723
|
+
*
|
|
1724
|
+
* Post-analysis derives several per-track "best" artefacts (best FRAME thumbnail,
|
|
1725
|
+
* best OBJECT/CLIP embedding + its crop, best FACE crop). They all rank the same
|
|
1726
|
+
* way: highest detector confidence per `trackId`. This selector composes the
|
|
1727
|
+
* canonical {@link BestDetectionTracker} ranking with the RESOLVED payload the
|
|
1728
|
+
* consumers need at the peak (bbox / className / embedding / faceBbox), so a
|
|
1729
|
+
* single "new best" DECISION can drive one capture whose output is shared:
|
|
1730
|
+
* - the boxed best-frame `thumbnail`,
|
|
1731
|
+
* - the tight object crop written onto the CLIP embedding row's `mediaKey`
|
|
1732
|
+
* (so a search hit's thumbnail IS the embedded crop).
|
|
1733
|
+
*
|
|
1734
|
+
* It is deliberately in-memory (the peak is per live track, dropped at track
|
|
1735
|
+
* end). The CLIP embedding's cross-restart persistence stays a SEPARATE store-
|
|
1736
|
+
* side gate (`ObjectEmbeddingStore.upsertIfBetter`): this selector unifies the
|
|
1737
|
+
* DECISION, not the durable store (see best-detection-tracker.ts docstring).
|
|
1738
|
+
*
|
|
1739
|
+
* The face path keeps its own hold buffer because a face crop can only come
|
|
1740
|
+
* from a face-bearing frame (the documented FRAME↔FACE seam) — but it shares
|
|
1741
|
+
* this ranking so best-frame and best-face agree on which frame is "best".
|
|
1742
|
+
*/
|
|
1743
|
+
var TrackBestSelector = class {
|
|
1744
|
+
tracker;
|
|
1745
|
+
payloads = /* @__PURE__ */ new Map();
|
|
1746
|
+
constructor(options = {}) {
|
|
1747
|
+
this.tracker = new BestDetectionTracker(options);
|
|
1748
|
+
}
|
|
1749
|
+
/**
|
|
1750
|
+
* Record an observation for its track. Returns true when it becomes the
|
|
1751
|
+
* track's new best (first sighting, or a confidence that beats the held peak
|
|
1752
|
+
* per the tracker's hysteresis / minGap rules). On acceptance the held payload
|
|
1753
|
+
* advances to this observation so `peak(trackId)` returns the winning frame's
|
|
1754
|
+
* bbox / embedding / faceBbox.
|
|
1755
|
+
*/
|
|
1756
|
+
observe(obs) {
|
|
1757
|
+
const isNewBest = this.tracker.observe(obs.trackId, obs.confidence, obs.atMs);
|
|
1758
|
+
if (isNewBest) {
|
|
1759
|
+
const { trackId, ...payload } = obs;
|
|
1760
|
+
this.payloads.set(trackId, payload);
|
|
1761
|
+
}
|
|
1762
|
+
return isNewBest;
|
|
1763
|
+
}
|
|
1764
|
+
/** The held best payload for a track (undefined if never observed). */
|
|
1765
|
+
peak(trackId) {
|
|
1766
|
+
return this.payloads.get(trackId);
|
|
1767
|
+
}
|
|
1768
|
+
/** The held peak confidence for a track (undefined if never observed). */
|
|
1769
|
+
peakConfidence(trackId) {
|
|
1770
|
+
return this.tracker.peak(trackId)?.confidence;
|
|
1771
|
+
}
|
|
1772
|
+
/** Drop a track's peak + payload (call at track end). */
|
|
1773
|
+
delete(trackId) {
|
|
1774
|
+
this.tracker.delete(trackId);
|
|
1775
|
+
this.payloads.delete(trackId);
|
|
1776
|
+
}
|
|
1777
|
+
clear() {
|
|
1778
|
+
this.tracker.clear();
|
|
1779
|
+
this.payloads.clear();
|
|
1780
|
+
}
|
|
1781
|
+
};
|
|
1782
|
+
//#endregion
|
|
1783
|
+
//#region src/pipeline-analytics/pipeline/object-embedding-selection.ts
|
|
1784
|
+
function isClipObjectEmbedding(t) {
|
|
1785
|
+
return Array.isArray(t.embedding) && t.embedding.length > 0 && typeof t.embeddingModelId === "string" && t.embeddingModelId.startsWith("mobileclip-");
|
|
1786
|
+
}
|
|
1787
|
+
function resolveSearchThumbnailUrl(input) {
|
|
1788
|
+
if (input.baseUrl === null) return void 0;
|
|
1789
|
+
const id = input.embeddingMediaKey ?? input.eventId;
|
|
1790
|
+
return `${input.baseUrl}/${encodeURIComponent(id)}`;
|
|
1791
|
+
}
|
|
1792
|
+
//#endregion
|
|
1793
|
+
//#region src/pipeline-analytics/pipeline/key-event-query.ts
|
|
1794
|
+
async function rankKeyEvents(candidates, options, peakLookup) {
|
|
1795
|
+
const scored = [];
|
|
1796
|
+
for (const t of candidates) {
|
|
1797
|
+
if (options.classFilter !== void 0 && t.className !== options.classFilter) continue;
|
|
1798
|
+
let importance = t.importance;
|
|
1799
|
+
let bestEventId = t.bestEventId;
|
|
1800
|
+
if (importance === void 0) {
|
|
1801
|
+
const peak = await peakLookup(t.trackId);
|
|
1802
|
+
importance = computeImportance({
|
|
1803
|
+
peakConfidence: peak.peakConfidence,
|
|
1804
|
+
className: t.className,
|
|
1805
|
+
durationMs: t.lastSeen - t.firstSeen,
|
|
1806
|
+
peakBboxAreaFrac: peak.peakBboxAreaFrac,
|
|
1807
|
+
totalDistance: t.totalDistance,
|
|
1808
|
+
zonesVisited: t.zonesVisited,
|
|
1809
|
+
...t.label !== void 0 ? { label: t.label } : {}
|
|
1810
|
+
}).importance;
|
|
1811
|
+
bestEventId = bestEventId ?? peak.bestEventId;
|
|
1812
|
+
}
|
|
1813
|
+
if (options.minImportance !== void 0 && importance < options.minImportance) continue;
|
|
1814
|
+
scored.push({
|
|
1815
|
+
id: bestEventId ?? t.trackId,
|
|
1816
|
+
trackId: t.trackId,
|
|
1817
|
+
timestamp: t.firstSeen,
|
|
1818
|
+
className: t.className,
|
|
1819
|
+
...t.label !== void 0 ? { label: t.label } : {},
|
|
1820
|
+
importance,
|
|
1821
|
+
bestEventId: bestEventId ?? "",
|
|
1822
|
+
windowMs: t.lastSeen - t.firstSeen
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
scored.sort((a, b) => b.importance - a.importance);
|
|
1826
|
+
return scored.slice(0, options.limit);
|
|
1827
|
+
}
|
|
1828
|
+
//#endregion
|
|
1572
1829
|
//#region src/pipeline-analytics/pipeline/native-detection.ts
|
|
1573
1830
|
/**
|
|
1574
1831
|
* Nominal frame size used to denormalize native `[0,1]` boxes when a
|
|
@@ -1738,6 +1995,18 @@ var TRACKS_COLUMNS = [
|
|
|
1738
1995
|
{
|
|
1739
1996
|
name: "state",
|
|
1740
1997
|
type: "TEXT"
|
|
1998
|
+
},
|
|
1999
|
+
{
|
|
2000
|
+
name: "importance",
|
|
2001
|
+
type: "REAL"
|
|
2002
|
+
},
|
|
2003
|
+
{
|
|
2004
|
+
name: "bestEventId",
|
|
2005
|
+
type: "TEXT"
|
|
2006
|
+
},
|
|
2007
|
+
{
|
|
2008
|
+
name: "importanceReason",
|
|
2009
|
+
type: "TEXT"
|
|
1741
2010
|
}
|
|
1742
2011
|
];
|
|
1743
2012
|
var TRACKS_INDEXES = [{
|
|
@@ -1769,7 +2038,10 @@ function cloneTrack(t) {
|
|
|
1769
2038
|
zonesVisited: [...t.zonesVisited],
|
|
1770
2039
|
totalDistance: t.totalDistance,
|
|
1771
2040
|
state: t.state,
|
|
1772
|
-
active: t.active
|
|
2041
|
+
active: t.active,
|
|
2042
|
+
...t.importance !== void 0 ? { importance: t.importance } : {},
|
|
2043
|
+
...t.bestEventId !== void 0 ? { bestEventId: t.bestEventId } : {},
|
|
2044
|
+
...t.importanceReason !== void 0 ? { importanceReason: t.importanceReason } : {}
|
|
1773
2045
|
};
|
|
1774
2046
|
}
|
|
1775
2047
|
var TrackStore = class {
|
|
@@ -1903,6 +2175,37 @@ var TrackStore = class {
|
|
|
1903
2175
|
}
|
|
1904
2176
|
}
|
|
1905
2177
|
/**
|
|
2178
|
+
* Stamp a track's importance score (+ dominant reason and best-event pointer).
|
|
2179
|
+
* Updates the in-memory active entry (so the value is carried into an eventual
|
|
2180
|
+
* (re)persist) AND patches the already-persisted row. Mirrors `setLabel`.
|
|
2181
|
+
* Forward-only — never rewrites history beyond these fields.
|
|
2182
|
+
*/
|
|
2183
|
+
async setImportance(trackId, importance, reason, bestEventId) {
|
|
2184
|
+
const active = this.active.get(trackId);
|
|
2185
|
+
if (active) {
|
|
2186
|
+
active.importance = importance;
|
|
2187
|
+
active.importanceReason = reason;
|
|
2188
|
+
if (bestEventId !== void 0) active.bestEventId = bestEventId;
|
|
2189
|
+
}
|
|
2190
|
+
const data = {
|
|
2191
|
+
importance,
|
|
2192
|
+
importanceReason: reason
|
|
2193
|
+
};
|
|
2194
|
+
if (bestEventId !== void 0) data["bestEventId"] = bestEventId;
|
|
2195
|
+
try {
|
|
2196
|
+
await this.store.update.mutate({
|
|
2197
|
+
collection: TRACKS_COLLECTION,
|
|
2198
|
+
id: trackId,
|
|
2199
|
+
data
|
|
2200
|
+
});
|
|
2201
|
+
} catch (err) {
|
|
2202
|
+
this.logger.warn("setImportance persist failed", { meta: {
|
|
2203
|
+
trackId,
|
|
2204
|
+
error: String(err)
|
|
2205
|
+
} });
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
/**
|
|
1906
2209
|
* Clear a track's label. Sets label to null in the persisted row (so
|
|
1907
2210
|
* rowToTrack's `typeof label === 'string'` guard omits it on read → label
|
|
1908
2211
|
* is absent/undefined). Also clears the in-memory active entry if present.
|
|
@@ -1973,7 +2276,10 @@ var TrackStore = class {
|
|
|
1973
2276
|
snapshots: [...t.snapshots],
|
|
1974
2277
|
zonesVisited: [...t.zonesVisited],
|
|
1975
2278
|
totalDistance: t.totalDistance,
|
|
1976
|
-
state: t.state
|
|
2279
|
+
state: t.state,
|
|
2280
|
+
...t.importance !== void 0 ? { importance: t.importance } : {},
|
|
2281
|
+
...t.bestEventId !== void 0 ? { bestEventId: t.bestEventId } : {},
|
|
2282
|
+
...t.importanceReason !== void 0 ? { importanceReason: t.importanceReason } : {}
|
|
1977
2283
|
}
|
|
1978
2284
|
});
|
|
1979
2285
|
}
|
|
@@ -1982,6 +2288,9 @@ var TrackStore = class {
|
|
|
1982
2288
|
const snapshots = data["snapshots"] ?? [];
|
|
1983
2289
|
const zones = data["zonesVisited"] ?? [];
|
|
1984
2290
|
const label = data["label"];
|
|
2291
|
+
const importance = data["importance"];
|
|
2292
|
+
const bestEventId = data["bestEventId"];
|
|
2293
|
+
const importanceReason = data["importanceReason"];
|
|
1985
2294
|
return {
|
|
1986
2295
|
trackId: id,
|
|
1987
2296
|
deviceId: Number(data["deviceId"]),
|
|
@@ -1994,7 +2303,10 @@ var TrackStore = class {
|
|
|
1994
2303
|
zonesVisited: zones,
|
|
1995
2304
|
totalDistance: Number(data["totalDistance"] ?? 0),
|
|
1996
2305
|
state: data["state"] ?? "idle",
|
|
1997
|
-
active: false
|
|
2306
|
+
active: false,
|
|
2307
|
+
...typeof importance === "number" ? { importance } : {},
|
|
2308
|
+
...typeof bestEventId === "string" ? { bestEventId } : {},
|
|
2309
|
+
...typeof importanceReason === "string" ? { importanceReason } : {}
|
|
1998
2310
|
};
|
|
1999
2311
|
}
|
|
2000
2312
|
};
|
|
@@ -2497,6 +2809,10 @@ var OBJECT_COLUMNS = [
|
|
|
2497
2809
|
{
|
|
2498
2810
|
name: "mediaKey",
|
|
2499
2811
|
type: "TEXT"
|
|
2812
|
+
},
|
|
2813
|
+
{
|
|
2814
|
+
name: "importance",
|
|
2815
|
+
type: "REAL"
|
|
2500
2816
|
}
|
|
2501
2817
|
];
|
|
2502
2818
|
var AUDIO_COLUMNS = [
|
|
@@ -2694,6 +3010,63 @@ var EventStore = class {
|
|
|
2694
3010
|
return updated;
|
|
2695
3011
|
}
|
|
2696
3012
|
/**
|
|
3013
|
+
* Forward-only: stamp `importance` on every already-emitted object event of a
|
|
3014
|
+
* track. Returns the number of events updated. Best-effort per row. Mirrors
|
|
3015
|
+
* `setLabelForTrack`. Called when a track's key-event score is computed (at
|
|
3016
|
+
* expiry) or recomputed (late label) so an event row carries the parent
|
|
3017
|
+
* track's importance without a join.
|
|
3018
|
+
*/
|
|
3019
|
+
async setImportanceForTrack(trackId, importance) {
|
|
3020
|
+
const rows = await this.store.query.query({
|
|
3021
|
+
collection: OBJECT_EVENTS_COLLECTION,
|
|
3022
|
+
filter: { where: { trackId } }
|
|
3023
|
+
});
|
|
3024
|
+
let updated = 0;
|
|
3025
|
+
for (const row of rows) try {
|
|
3026
|
+
await this.store.update.mutate({
|
|
3027
|
+
collection: OBJECT_EVENTS_COLLECTION,
|
|
3028
|
+
id: row.id,
|
|
3029
|
+
data: { importance }
|
|
3030
|
+
});
|
|
3031
|
+
updated++;
|
|
3032
|
+
} catch (err) {
|
|
3033
|
+
this.logger.warn("setImportanceForTrack update failed", { meta: {
|
|
3034
|
+
trackId,
|
|
3035
|
+
eventId: row.id,
|
|
3036
|
+
error: String(err)
|
|
3037
|
+
} });
|
|
3038
|
+
}
|
|
3039
|
+
return updated;
|
|
3040
|
+
}
|
|
3041
|
+
/**
|
|
3042
|
+
* The track's highest-confidence object event, its bbox area (as a fraction of
|
|
3043
|
+
* frame area), and that event's id — the SHARED per-track ranking already used
|
|
3044
|
+
* for the best frame, read back from the persisted object events (index
|
|
3045
|
+
* `idx_object_track`). Returns zeros + undefined id when the track has none.
|
|
3046
|
+
* Used by the importance scorer at expiry and by `getKeyEvents` compute-on-read.
|
|
3047
|
+
*/
|
|
3048
|
+
async peakForTrack(trackId) {
|
|
3049
|
+
const rows = await this.store.query.query({
|
|
3050
|
+
collection: OBJECT_EVENTS_COLLECTION,
|
|
3051
|
+
filter: { where: { trackId } }
|
|
3052
|
+
});
|
|
3053
|
+
let bestConf = -1;
|
|
3054
|
+
let bestEventId;
|
|
3055
|
+
let peakBboxAreaFrac = 0;
|
|
3056
|
+
for (const row of rows) {
|
|
3057
|
+
const conf = typeof row.data["confidence"] === "number" ? row.data["confidence"] : 0;
|
|
3058
|
+
if (conf <= bestConf) continue;
|
|
3059
|
+
bestConf = conf;
|
|
3060
|
+
bestEventId = row.id;
|
|
3061
|
+
peakBboxAreaFrac = bboxAreaFrac(row.data);
|
|
3062
|
+
}
|
|
3063
|
+
return {
|
|
3064
|
+
peakConfidence: bestConf < 0 ? 0 : bestConf,
|
|
3065
|
+
peakBboxAreaFrac,
|
|
3066
|
+
bestEventId
|
|
3067
|
+
};
|
|
3068
|
+
}
|
|
3069
|
+
/**
|
|
2697
3070
|
* Clear `label` on every already-emitted object event of a track (sets label
|
|
2698
3071
|
* to null so stripNulls/slimObject omit it on read → label is absent/undefined).
|
|
2699
3072
|
* Returns the number of events updated. Best-effort per row. Mirrors
|
|
@@ -2888,7 +3261,8 @@ function slimObject(id, data) {
|
|
|
2888
3261
|
timestamp: data["timestamp"],
|
|
2889
3262
|
className: data["className"],
|
|
2890
3263
|
...typeof data["frameId"] === "string" ? { frameId: data["frameId"] } : {},
|
|
2891
|
-
...typeof data["trackId"] === "string" ? { trackId: data["trackId"] } : {}
|
|
3264
|
+
...typeof data["trackId"] === "string" ? { trackId: data["trackId"] } : {},
|
|
3265
|
+
...typeof data["importance"] === "number" ? { importance: data["importance"] } : {}
|
|
2892
3266
|
};
|
|
2893
3267
|
if (typeof data["label"] === "string") return {
|
|
2894
3268
|
...base,
|
|
@@ -2918,6 +3292,17 @@ function slimAudio(id, data) {
|
|
|
2918
3292
|
}
|
|
2919
3293
|
return base;
|
|
2920
3294
|
}
|
|
3295
|
+
function bboxAreaFrac(data) {
|
|
3296
|
+
const bbox = data["bbox"];
|
|
3297
|
+
const fw = data["frameWidth"];
|
|
3298
|
+
const fh = data["frameHeight"];
|
|
3299
|
+
if (bbox === null || typeof bbox !== "object") return 0;
|
|
3300
|
+
if (typeof fw !== "number" || typeof fh !== "number" || fw <= 0 || fh <= 0) return 0;
|
|
3301
|
+
const w = "w" in bbox && typeof bbox.w === "number" ? bbox.w : 0;
|
|
3302
|
+
const h = "h" in bbox && typeof bbox.h === "number" ? bbox.h : 0;
|
|
3303
|
+
if (w <= 0 || h <= 0) return 0;
|
|
3304
|
+
return w * h / (fw * fh);
|
|
3305
|
+
}
|
|
2921
3306
|
function stripNulls(data) {
|
|
2922
3307
|
const out = {};
|
|
2923
3308
|
for (const [k, v] of Object.entries(data)) if (v !== null) out[k] = v;
|
|
@@ -4537,6 +4922,14 @@ var FACE_COLUMNS = [
|
|
|
4537
4922
|
{
|
|
4538
4923
|
name: "assignedSampleId",
|
|
4539
4924
|
type: "TEXT"
|
|
4925
|
+
},
|
|
4926
|
+
{
|
|
4927
|
+
name: "keyFrameMediaKey",
|
|
4928
|
+
type: "TEXT"
|
|
4929
|
+
},
|
|
4930
|
+
{
|
|
4931
|
+
name: "faceBbox",
|
|
4932
|
+
type: "JSON"
|
|
4540
4933
|
}
|
|
4541
4934
|
];
|
|
4542
4935
|
var FACE_INDEXES = [{
|
|
@@ -4609,7 +5002,9 @@ var FaceStore = class {
|
|
|
4609
5002
|
assigned: Boolean(r.data.assigned),
|
|
4610
5003
|
mediaKey: data.mediaKey ?? void 0,
|
|
4611
5004
|
recognizedIdentityId: data.recognizedIdentityId ?? void 0,
|
|
4612
|
-
assignedSampleId: data.assignedSampleId ?? void 0
|
|
5005
|
+
assignedSampleId: data.assignedSampleId ?? void 0,
|
|
5006
|
+
keyFrameMediaKey: data.keyFrameMediaKey ?? void 0,
|
|
5007
|
+
faceBbox: data.faceBbox ?? void 0
|
|
4613
5008
|
};
|
|
4614
5009
|
}).filter((f) => !f.assigned);
|
|
4615
5010
|
}
|
|
@@ -4773,8 +5168,11 @@ var FaceStore = class {
|
|
|
4773
5168
|
id: faceId,
|
|
4774
5169
|
...data,
|
|
4775
5170
|
assigned: Boolean(raw.assigned),
|
|
5171
|
+
mediaKey: data.mediaKey ?? void 0,
|
|
4776
5172
|
recognizedIdentityId: data.recognizedIdentityId ?? void 0,
|
|
4777
|
-
assignedSampleId: data.assignedSampleId ?? void 0
|
|
5173
|
+
assignedSampleId: data.assignedSampleId ?? void 0,
|
|
5174
|
+
keyFrameMediaKey: data.keyFrameMediaKey ?? void 0,
|
|
5175
|
+
faceBbox: data.faceBbox ?? void 0
|
|
4778
5176
|
};
|
|
4779
5177
|
}
|
|
4780
5178
|
/**
|
|
@@ -4818,7 +5216,9 @@ var FaceStore = class {
|
|
|
4818
5216
|
assigned: Boolean(r.data.assigned),
|
|
4819
5217
|
mediaKey: data.mediaKey ?? void 0,
|
|
4820
5218
|
recognizedIdentityId: data.recognizedIdentityId ?? void 0,
|
|
4821
|
-
assignedSampleId: data.assignedSampleId ?? void 0
|
|
5219
|
+
assignedSampleId: data.assignedSampleId ?? void 0,
|
|
5220
|
+
keyFrameMediaKey: data.keyFrameMediaKey ?? void 0,
|
|
5221
|
+
faceBbox: data.faceBbox ?? void 0
|
|
4822
5222
|
};
|
|
4823
5223
|
});
|
|
4824
5224
|
const filterMode = input.filter ?? "all";
|
|
@@ -4883,6 +5283,10 @@ var OBJECT_EMBEDDING_COLUMNS = [
|
|
|
4883
5283
|
{
|
|
4884
5284
|
name: "mediaKey",
|
|
4885
5285
|
type: "TEXT"
|
|
5286
|
+
},
|
|
5287
|
+
{
|
|
5288
|
+
name: "keyFrameMediaKey",
|
|
5289
|
+
type: "TEXT"
|
|
4886
5290
|
}
|
|
4887
5291
|
];
|
|
4888
5292
|
var ObjectEmbeddingStore = class {
|
|
@@ -4929,7 +5333,8 @@ var ObjectEmbeddingStore = class {
|
|
|
4929
5333
|
modelId: input.modelId,
|
|
4930
5334
|
dim: input.embedding.length,
|
|
4931
5335
|
confidence: input.confidence,
|
|
4932
|
-
...input.mediaKey !== void 0 ? { mediaKey: input.mediaKey } : {}
|
|
5336
|
+
...input.mediaKey !== void 0 ? { mediaKey: input.mediaKey } : {},
|
|
5337
|
+
...input.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: input.keyFrameMediaKey } : {}
|
|
4933
5338
|
};
|
|
4934
5339
|
try {
|
|
4935
5340
|
await this.store.set.mutate({
|
|
@@ -4967,7 +5372,8 @@ var ObjectEmbeddingStore = class {
|
|
|
4967
5372
|
return {
|
|
4968
5373
|
id: r.id,
|
|
4969
5374
|
...data,
|
|
4970
|
-
mediaKey: data.mediaKey ?? void 0
|
|
5375
|
+
mediaKey: data.mediaKey ?? void 0,
|
|
5376
|
+
keyFrameMediaKey: data.keyFrameMediaKey ?? void 0
|
|
4971
5377
|
};
|
|
4972
5378
|
});
|
|
4973
5379
|
}
|
|
@@ -5096,6 +5502,16 @@ function updateTrackAggregate(prev, match, opts) {
|
|
|
5096
5502
|
}
|
|
5097
5503
|
//#endregion
|
|
5098
5504
|
//#region src/pipeline-analytics/face-recognizer.ts
|
|
5505
|
+
/** At most one "dropping imageless track" log per this interval, per recognizer. */
|
|
5506
|
+
var FACE_IMAGELESS_LOG_THROTTLE_MS = 6e4;
|
|
5507
|
+
/**
|
|
5508
|
+
* arcface model id stamped on a detail-plane face candidate when the gallery is
|
|
5509
|
+
* empty (collect-only). The detail-subtree result carries no `embeddingModelId`
|
|
5510
|
+
* (the two-plane `DetailResult` schema omits it), so recognition uses the
|
|
5511
|
+
* gallery's own model id (all enrolled samples share one) and this constant is
|
|
5512
|
+
* only a placeholder for the collect-only case where the id is never compared.
|
|
5513
|
+
*/
|
|
5514
|
+
var FALLBACK_FACE_MODEL_ID = "arcface";
|
|
5099
5515
|
var FaceRecognizer = class {
|
|
5100
5516
|
deps;
|
|
5101
5517
|
gallery = [];
|
|
@@ -5107,6 +5523,9 @@ var FaceRecognizer = class {
|
|
|
5107
5523
|
* true highest-confidence face (holding a buffer in memory is cheap, and a
|
|
5108
5524
|
* track may last well under the best-frame rate-limit window). */
|
|
5109
5525
|
bestTracker = new BestDetectionTracker();
|
|
5526
|
+
/** Throttle for the "dropping imageless track" log — one line per minute at
|
|
5527
|
+
* most, so a busy scene that never produces a face crop can't flood logs. */
|
|
5528
|
+
lastImagelessLogAt = 0;
|
|
5110
5529
|
constructor(deps) {
|
|
5111
5530
|
this.deps = deps;
|
|
5112
5531
|
}
|
|
@@ -5120,6 +5539,38 @@ var FaceRecognizer = class {
|
|
|
5120
5539
|
this.deps.logger.warn("FaceRecognizer.refreshGallery failed", { meta: { error: String(err) } });
|
|
5121
5540
|
}
|
|
5122
5541
|
}
|
|
5542
|
+
/**
|
|
5543
|
+
* Two-plane detail feed: ingest ONE `runDetailSubtree` face result for a
|
|
5544
|
+
* track and run it through the SAME `processFrame` logic (candidate → best
|
|
5545
|
+
* face → gallery match → crop hold). The result is synthesized into a single
|
|
5546
|
+
* `TrackedDetectionOut` candidate so no recognizer logic changes — only the
|
|
5547
|
+
* input source moves from the per-frame plane to this per-track call.
|
|
5548
|
+
*/
|
|
5549
|
+
async ingestFaceDetail(input) {
|
|
5550
|
+
const modelId = this.gallery[0]?.modelId ?? FALLBACK_FACE_MODEL_ID;
|
|
5551
|
+
const candidate = {
|
|
5552
|
+
trackId: input.trackId,
|
|
5553
|
+
className: "face",
|
|
5554
|
+
confidence: input.score,
|
|
5555
|
+
bbox: input.parentBbox,
|
|
5556
|
+
zones: [],
|
|
5557
|
+
state: "moving",
|
|
5558
|
+
embedding: input.embedding,
|
|
5559
|
+
embeddingModelId: modelId,
|
|
5560
|
+
...input.faceBbox !== void 0 ? { faceBbox: input.faceBbox } : {},
|
|
5561
|
+
...input.alignedCropJpeg !== void 0 ? { faceAlignedCrop: input.alignedCropJpeg } : {}
|
|
5562
|
+
};
|
|
5563
|
+
await this.processFrame({
|
|
5564
|
+
deviceId: input.deviceId,
|
|
5565
|
+
timestamp: input.timestamp,
|
|
5566
|
+
frameWidth: input.frameWidth,
|
|
5567
|
+
frameHeight: input.frameHeight,
|
|
5568
|
+
tracked: [candidate],
|
|
5569
|
+
settings: input.settings,
|
|
5570
|
+
cropPadding: input.cropPadding,
|
|
5571
|
+
...input.frameHandle !== void 0 ? { frameHandle: input.frameHandle } : {}
|
|
5572
|
+
});
|
|
5573
|
+
}
|
|
5123
5574
|
async processFrame(input) {
|
|
5124
5575
|
const { settings } = input;
|
|
5125
5576
|
const candidates = input.tracked.filter((t) => Array.isArray(t.embedding) && t.embedding.length > 0 && typeof t.embeddingModelId === "string" && t.confidence >= settings.minFaceConfidence);
|
|
@@ -5157,7 +5608,8 @@ var FaceRecognizer = class {
|
|
|
5157
5608
|
if (isNewBest || needsCrop) {
|
|
5158
5609
|
const cropBbox = c.faceBbox ?? c.bbox;
|
|
5159
5610
|
let crop;
|
|
5160
|
-
if (c.
|
|
5611
|
+
if (c.faceAlignedCrop !== void 0) crop = Buffer.from(c.faceAlignedCrop, "base64");
|
|
5612
|
+
else if (c.faceBbox !== void 0 && input.frameHandle !== void 0) try {
|
|
5161
5613
|
crop = await this.deps.captureCrop(input.frameHandle, c.faceBbox, input.frameWidth, input.frameHeight, input.cropPadding) ?? void 0;
|
|
5162
5614
|
} catch (err) {
|
|
5163
5615
|
this.deps.logger.debug("FaceRecognizer crop capture failed", {
|
|
@@ -5236,6 +5688,17 @@ var FaceRecognizer = class {
|
|
|
5236
5688
|
}
|
|
5237
5689
|
});
|
|
5238
5690
|
}
|
|
5691
|
+
try {
|
|
5692
|
+
await this.deps.recomputeImportance?.(work.trackId);
|
|
5693
|
+
} catch (err) {
|
|
5694
|
+
this.deps.logger.warn("recomputeImportance failed", {
|
|
5695
|
+
tags: { deviceId: input.deviceId },
|
|
5696
|
+
meta: {
|
|
5697
|
+
trackId: work.trackId,
|
|
5698
|
+
error: String(err)
|
|
5699
|
+
}
|
|
5700
|
+
});
|
|
5701
|
+
}
|
|
5239
5702
|
}
|
|
5240
5703
|
}
|
|
5241
5704
|
/**
|
|
@@ -5255,9 +5718,23 @@ var FaceRecognizer = class {
|
|
|
5255
5718
|
} });
|
|
5256
5719
|
return;
|
|
5257
5720
|
}
|
|
5721
|
+
if (held.crop === void 0) {
|
|
5722
|
+
const now = Date.now();
|
|
5723
|
+
if (now - this.lastImagelessLogAt >= FACE_IMAGELESS_LOG_THROTTLE_MS) {
|
|
5724
|
+
this.lastImagelessLogAt = now;
|
|
5725
|
+
this.deps.logger.info("face: dropping imageless track (no crop captured)", {
|
|
5726
|
+
tags: {
|
|
5727
|
+
deviceId,
|
|
5728
|
+
trackId
|
|
5729
|
+
},
|
|
5730
|
+
meta: { score: held.score }
|
|
5731
|
+
});
|
|
5732
|
+
}
|
|
5733
|
+
return;
|
|
5734
|
+
}
|
|
5258
5735
|
const faceId = `face-${trackId}`;
|
|
5259
5736
|
let mediaKey;
|
|
5260
|
-
|
|
5737
|
+
try {
|
|
5261
5738
|
mediaKey = await this.deps.mediaStore.put({
|
|
5262
5739
|
deviceId,
|
|
5263
5740
|
ownerKind: "face",
|
|
@@ -5275,6 +5752,17 @@ var FaceRecognizer = class {
|
|
|
5275
5752
|
}
|
|
5276
5753
|
});
|
|
5277
5754
|
}
|
|
5755
|
+
if (mediaKey === void 0) {
|
|
5756
|
+
this.deps.logger.warn("face: crop store failed — dropping face row", {
|
|
5757
|
+
tags: {
|
|
5758
|
+
deviceId,
|
|
5759
|
+
trackId
|
|
5760
|
+
},
|
|
5761
|
+
meta: { faceId }
|
|
5762
|
+
});
|
|
5763
|
+
return;
|
|
5764
|
+
}
|
|
5765
|
+
const keyFrameMediaKey = this.deps.getKeyFrameMediaKey?.(trackId);
|
|
5278
5766
|
try {
|
|
5279
5767
|
await this.deps.faceStore.insert({
|
|
5280
5768
|
id: faceId,
|
|
@@ -5282,9 +5770,11 @@ var FaceRecognizer = class {
|
|
|
5282
5770
|
trackId,
|
|
5283
5771
|
timestamp: held.timestamp,
|
|
5284
5772
|
embedding: held.embedding,
|
|
5285
|
-
|
|
5773
|
+
mediaKey,
|
|
5286
5774
|
...held.recognizedIdentityId !== void 0 ? { recognizedIdentityId: held.recognizedIdentityId } : {},
|
|
5287
|
-
assigned: false
|
|
5775
|
+
assigned: false,
|
|
5776
|
+
faceBbox: held.bbox,
|
|
5777
|
+
...keyFrameMediaKey !== void 0 ? { keyFrameMediaKey } : {}
|
|
5288
5778
|
});
|
|
5289
5779
|
this.deps.logger.info("face: buffered to gallery", {
|
|
5290
5780
|
tags: {
|
|
@@ -5293,7 +5783,7 @@ var FaceRecognizer = class {
|
|
|
5293
5783
|
},
|
|
5294
5784
|
meta: {
|
|
5295
5785
|
faceId,
|
|
5296
|
-
hasCrop:
|
|
5786
|
+
hasCrop: true,
|
|
5297
5787
|
recognizedIdentityId: held.recognizedIdentityId ?? null,
|
|
5298
5788
|
score: held.score
|
|
5299
5789
|
}
|
|
@@ -5310,6 +5800,346 @@ var FaceRecognizer = class {
|
|
|
5310
5800
|
}
|
|
5311
5801
|
};
|
|
5312
5802
|
//#endregion
|
|
5803
|
+
//#region src/pipeline-analytics/detail-scheduler.ts
|
|
5804
|
+
/** Default backoff/period when a step's cadence omits `minIntervalMs`. */
|
|
5805
|
+
var DEFAULT_MIN_INTERVAL_MS = 1e3;
|
|
5806
|
+
/** Default fire cap for a `once` step when its announce omits `maxPerTrack`. */
|
|
5807
|
+
var DEFAULT_ONCE_MAX_PER_TRACK = 3;
|
|
5808
|
+
/**
|
|
5809
|
+
* Pure per-(track, step) scheduling state machine for detail-subtree
|
|
5810
|
+
* dispatch. Given a camera's announced child steps (`DetailStepAnnounce[]`,
|
|
5811
|
+
* read off `PipelineInferenceResultPayload.detailSteps`), decides WHEN each
|
|
5812
|
+
* step should run for a given track — independent of transport, I/O, or
|
|
5813
|
+
* timers. The caller drives it with wall-clock `nowMs` and dispatches the
|
|
5814
|
+
* returned `DetailRequest[]`.
|
|
5815
|
+
*/
|
|
5816
|
+
var DetailScheduler = class {
|
|
5817
|
+
tracks = /* @__PURE__ */ new Map();
|
|
5818
|
+
/** Track appeared with class + announce; returns immediate requests. */
|
|
5819
|
+
onTrackStarted(trackId, className, announce, nowMs) {
|
|
5820
|
+
const steps = /* @__PURE__ */ new Map();
|
|
5821
|
+
const requests = [];
|
|
5822
|
+
for (const stepAnnounce of announce) {
|
|
5823
|
+
if (!stepAnnounce.inputClasses.includes(className)) continue;
|
|
5824
|
+
const state = {
|
|
5825
|
+
announce: stepAnnounce,
|
|
5826
|
+
firedCount: 1,
|
|
5827
|
+
lastFiredAt: nowMs,
|
|
5828
|
+
sticky: false,
|
|
5829
|
+
retryPending: false
|
|
5830
|
+
};
|
|
5831
|
+
steps.set(stepAnnounce.stepId, state);
|
|
5832
|
+
requests.push({
|
|
5833
|
+
trackId,
|
|
5834
|
+
stepId: stepAnnounce.stepId,
|
|
5835
|
+
reason: "new-track"
|
|
5836
|
+
});
|
|
5837
|
+
}
|
|
5838
|
+
this.tracks.set(trackId, steps);
|
|
5839
|
+
return requests;
|
|
5840
|
+
}
|
|
5841
|
+
/** Better candidate crop observed for the track. */
|
|
5842
|
+
onCandidateImproved(trackId, nowMs) {
|
|
5843
|
+
const steps = this.tracks.get(trackId);
|
|
5844
|
+
if (!steps) return [];
|
|
5845
|
+
const requests = [];
|
|
5846
|
+
for (const state of steps.values()) {
|
|
5847
|
+
if (state.announce.cadence.trigger !== "improve") continue;
|
|
5848
|
+
if (!this.canFire(state, nowMs)) continue;
|
|
5849
|
+
this.markFired(state, nowMs);
|
|
5850
|
+
requests.push({
|
|
5851
|
+
trackId,
|
|
5852
|
+
stepId: state.announce.stepId,
|
|
5853
|
+
reason: "improve"
|
|
5854
|
+
});
|
|
5855
|
+
}
|
|
5856
|
+
return requests;
|
|
5857
|
+
}
|
|
5858
|
+
/** Periodic tick (call ~1/s). Also carries pending retries for any trigger kind. */
|
|
5859
|
+
tick(nowMs) {
|
|
5860
|
+
const requests = [];
|
|
5861
|
+
for (const [trackId, steps] of this.tracks) for (const state of steps.values()) {
|
|
5862
|
+
if (state.sticky) continue;
|
|
5863
|
+
if (state.retryPending) {
|
|
5864
|
+
if (!this.intervalElapsed(state, nowMs)) continue;
|
|
5865
|
+
if (!this.underMaxPerTrack(state)) {
|
|
5866
|
+
state.retryPending = false;
|
|
5867
|
+
continue;
|
|
5868
|
+
}
|
|
5869
|
+
this.markFired(state, nowMs);
|
|
5870
|
+
requests.push({
|
|
5871
|
+
trackId,
|
|
5872
|
+
stepId: state.announce.stepId,
|
|
5873
|
+
reason: "retry"
|
|
5874
|
+
});
|
|
5875
|
+
continue;
|
|
5876
|
+
}
|
|
5877
|
+
if (state.announce.cadence.trigger !== "periodic") continue;
|
|
5878
|
+
if (!this.canFire(state, nowMs)) continue;
|
|
5879
|
+
this.markFired(state, nowMs);
|
|
5880
|
+
requests.push({
|
|
5881
|
+
trackId,
|
|
5882
|
+
stepId: state.announce.stepId,
|
|
5883
|
+
reason: "periodic"
|
|
5884
|
+
});
|
|
5885
|
+
}
|
|
5886
|
+
return requests;
|
|
5887
|
+
}
|
|
5888
|
+
/**
|
|
5889
|
+
* Result arrived; confidence drives sticky/retry. null = failed (retry per
|
|
5890
|
+
* policy). `_nowMs` is part of the public signature for symmetry with the
|
|
5891
|
+
* other methods but isn't needed here — retry backoff is anchored to
|
|
5892
|
+
* `lastFiredAt` (set when the step was actually dispatched), not to when
|
|
5893
|
+
* its result came back.
|
|
5894
|
+
*/
|
|
5895
|
+
onResult(trackId, stepId, confidence, _nowMs) {
|
|
5896
|
+
const steps = this.tracks.get(trackId);
|
|
5897
|
+
if (!steps) return;
|
|
5898
|
+
const state = steps.get(stepId);
|
|
5899
|
+
if (!state) return;
|
|
5900
|
+
if (state.sticky) return;
|
|
5901
|
+
const { stickyOnConfidence } = state.announce.cadence;
|
|
5902
|
+
if (confidence !== null && stickyOnConfidence !== void 0 && confidence >= stickyOnConfidence) {
|
|
5903
|
+
state.sticky = true;
|
|
5904
|
+
state.retryPending = false;
|
|
5905
|
+
return;
|
|
5906
|
+
}
|
|
5907
|
+
if (confidence === null) {
|
|
5908
|
+
if (this.underMaxPerTrack(state)) state.retryPending = true;
|
|
5909
|
+
return;
|
|
5910
|
+
}
|
|
5911
|
+
if (state.announce.cadence.trigger === "once" && stickyOnConfidence !== void 0) {
|
|
5912
|
+
if (this.underMaxPerTrack(state)) state.retryPending = true;
|
|
5913
|
+
}
|
|
5914
|
+
}
|
|
5915
|
+
onTrackEnded(trackId) {
|
|
5916
|
+
this.tracks.delete(trackId);
|
|
5917
|
+
}
|
|
5918
|
+
canFire(state, nowMs) {
|
|
5919
|
+
if (state.sticky) return false;
|
|
5920
|
+
if (!this.underMaxPerTrack(state)) return false;
|
|
5921
|
+
return this.intervalElapsed(state, nowMs);
|
|
5922
|
+
}
|
|
5923
|
+
intervalElapsed(state, nowMs) {
|
|
5924
|
+
if (state.lastFiredAt === null) return true;
|
|
5925
|
+
const minIntervalMs = state.announce.cadence.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS;
|
|
5926
|
+
return nowMs - state.lastFiredAt >= minIntervalMs;
|
|
5927
|
+
}
|
|
5928
|
+
underMaxPerTrack(state) {
|
|
5929
|
+
const maxPerTrack = state.announce.cadence.maxPerTrack ?? (state.announce.cadence.trigger === "once" ? DEFAULT_ONCE_MAX_PER_TRACK : Infinity);
|
|
5930
|
+
return state.firedCount < maxPerTrack;
|
|
5931
|
+
}
|
|
5932
|
+
markFired(state, nowMs) {
|
|
5933
|
+
state.firedCount += 1;
|
|
5934
|
+
state.lastFiredAt = nowMs;
|
|
5935
|
+
state.retryPending = false;
|
|
5936
|
+
}
|
|
5937
|
+
};
|
|
5938
|
+
//#endregion
|
|
5939
|
+
//#region src/pipeline-analytics/detail-dispatcher.ts
|
|
5940
|
+
/** Throttle for the per-device "detail call failed" warn — one line / minute. */
|
|
5941
|
+
var FAIL_WARN_THROTTLE_MS = 6e4;
|
|
5942
|
+
var TrackDetailDispatcher = class {
|
|
5943
|
+
deps;
|
|
5944
|
+
devices = /* @__PURE__ */ new Map();
|
|
5945
|
+
maxInFlight;
|
|
5946
|
+
tickIntervalMs;
|
|
5947
|
+
disposed = false;
|
|
5948
|
+
constructor(deps) {
|
|
5949
|
+
this.deps = deps;
|
|
5950
|
+
this.maxInFlight = deps.maxInFlightPerDevice ?? 2;
|
|
5951
|
+
this.tickIntervalMs = deps.tickIntervalMs ?? 1e3;
|
|
5952
|
+
}
|
|
5953
|
+
/** A track appeared: record its frame, seed its best-confidence, and dispatch
|
|
5954
|
+
* the scheduler's immediate (new-track) requests. */
|
|
5955
|
+
onTrackStarted(deviceId, trackId, className, announce, frame, nowMs) {
|
|
5956
|
+
if (this.disposed) return;
|
|
5957
|
+
const dev = this.ensureDevice(deviceId);
|
|
5958
|
+
dev.tracks.set(trackId, frame);
|
|
5959
|
+
dev.candidateBest.observe(trackId, frame.confidence, frame.timestamp);
|
|
5960
|
+
const requests = dev.scheduler.onTrackStarted(trackId, className, announce, nowMs);
|
|
5961
|
+
this.enqueue(deviceId, dev, requests);
|
|
5962
|
+
this.ensureTimer(deviceId, dev);
|
|
5963
|
+
}
|
|
5964
|
+
/** A subsequent frame for a live track: refresh its frame + fire
|
|
5965
|
+
* `improve`-cadence steps when the detector confidence strictly improves.
|
|
5966
|
+
*
|
|
5967
|
+
* `announce` is the frame's currently-announced detail chain. When a track
|
|
5968
|
+
* is alive but has NO dispatcher state yet — it existed before `detailSteps`
|
|
5969
|
+
* first appeared (a mid-track redeploy / config change) — this adopts it as a
|
|
5970
|
+
* new track so it starts getting scheduled instead of starving for its whole
|
|
5971
|
+
* life. Idempotent: guarded by the per-track state check, so a track that
|
|
5972
|
+
* already has state is never reset. */
|
|
5973
|
+
onFrame(deviceId, trackId, announce, frame, nowMs) {
|
|
5974
|
+
if (this.disposed) return;
|
|
5975
|
+
const dev = this.devices.get(deviceId);
|
|
5976
|
+
if (!dev || !dev.tracks.has(trackId)) {
|
|
5977
|
+
if (announce.length > 0) this.onTrackStarted(deviceId, trackId, frame.className, announce, frame, nowMs);
|
|
5978
|
+
return;
|
|
5979
|
+
}
|
|
5980
|
+
dev.tracks.set(trackId, frame);
|
|
5981
|
+
if (!dev.candidateBest.observe(trackId, frame.confidence, frame.timestamp)) return;
|
|
5982
|
+
const requests = dev.scheduler.onCandidateImproved(trackId, nowMs);
|
|
5983
|
+
this.enqueue(deviceId, dev, requests);
|
|
5984
|
+
}
|
|
5985
|
+
/** A track ended (durable TTL expiry): drop its scheduler + frame state. Any
|
|
5986
|
+
* queued request for it is discarded at dequeue. */
|
|
5987
|
+
onTrackEnded(deviceId, trackId) {
|
|
5988
|
+
const dev = this.devices.get(deviceId);
|
|
5989
|
+
if (!dev) return;
|
|
5990
|
+
dev.scheduler.onTrackEnded(trackId);
|
|
5991
|
+
dev.tracks.delete(trackId);
|
|
5992
|
+
dev.candidateBest.delete(trackId);
|
|
5993
|
+
if (dev.tracks.size === 0) this.clearTimer(dev);
|
|
5994
|
+
}
|
|
5995
|
+
dispose() {
|
|
5996
|
+
this.disposed = true;
|
|
5997
|
+
for (const dev of this.devices.values()) {
|
|
5998
|
+
this.clearTimer(dev);
|
|
5999
|
+
dev.queue.length = 0;
|
|
6000
|
+
dev.tracks.clear();
|
|
6001
|
+
dev.candidateBest.clear();
|
|
6002
|
+
}
|
|
6003
|
+
this.devices.clear();
|
|
6004
|
+
}
|
|
6005
|
+
ensureDevice(deviceId) {
|
|
6006
|
+
let dev = this.devices.get(deviceId);
|
|
6007
|
+
if (!dev) {
|
|
6008
|
+
dev = {
|
|
6009
|
+
scheduler: new DetailScheduler(),
|
|
6010
|
+
tracks: /* @__PURE__ */ new Map(),
|
|
6011
|
+
candidateBest: new BestDetectionTracker(),
|
|
6012
|
+
queue: [],
|
|
6013
|
+
inFlight: 0,
|
|
6014
|
+
timer: null,
|
|
6015
|
+
lastFailWarnAt: 0
|
|
6016
|
+
};
|
|
6017
|
+
this.devices.set(deviceId, dev);
|
|
6018
|
+
}
|
|
6019
|
+
return dev;
|
|
6020
|
+
}
|
|
6021
|
+
ensureTimer(deviceId, dev) {
|
|
6022
|
+
if (dev.timer) return;
|
|
6023
|
+
const timer = setInterval(() => this.tick(deviceId, dev), this.tickIntervalMs);
|
|
6024
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
6025
|
+
dev.timer = timer;
|
|
6026
|
+
}
|
|
6027
|
+
clearTimer(dev) {
|
|
6028
|
+
if (dev.timer) {
|
|
6029
|
+
clearInterval(dev.timer);
|
|
6030
|
+
dev.timer = null;
|
|
6031
|
+
}
|
|
6032
|
+
}
|
|
6033
|
+
tick(deviceId, dev) {
|
|
6034
|
+
if (this.disposed) return;
|
|
6035
|
+
const requests = dev.scheduler.tick(Date.now());
|
|
6036
|
+
this.enqueue(deviceId, dev, requests);
|
|
6037
|
+
}
|
|
6038
|
+
enqueue(deviceId, dev, requests) {
|
|
6039
|
+
if (requests.length === 0) return;
|
|
6040
|
+
for (const r of requests) dev.queue.push(r);
|
|
6041
|
+
this.pump(deviceId, dev);
|
|
6042
|
+
}
|
|
6043
|
+
pump(deviceId, dev) {
|
|
6044
|
+
while (dev.inFlight < this.maxInFlight && dev.queue.length > 0) {
|
|
6045
|
+
const req = dev.queue.shift();
|
|
6046
|
+
if (req === void 0) break;
|
|
6047
|
+
const frame = dev.tracks.get(req.trackId);
|
|
6048
|
+
if (frame === void 0) continue;
|
|
6049
|
+
dev.inFlight += 1;
|
|
6050
|
+
this.dispatch(deviceId, dev, req, frame).finally(() => {
|
|
6051
|
+
dev.inFlight -= 1;
|
|
6052
|
+
this.pump(deviceId, dev);
|
|
6053
|
+
});
|
|
6054
|
+
}
|
|
6055
|
+
}
|
|
6056
|
+
async dispatch(deviceId, dev, req, frame) {
|
|
6057
|
+
const details = await this.runOnce(deviceId, dev, req, frame);
|
|
6058
|
+
let topScore = null;
|
|
6059
|
+
if (details !== null && details.length > 0) {
|
|
6060
|
+
topScore = details.reduce((max, d) => d.score > max ? d.score : max, Number.NEGATIVE_INFINITY);
|
|
6061
|
+
try {
|
|
6062
|
+
await this.deps.routeResults(deviceId, req.trackId, details, frame);
|
|
6063
|
+
} catch (err) {
|
|
6064
|
+
this.deps.logger.warn("detail result routing failed", {
|
|
6065
|
+
tags: { deviceId },
|
|
6066
|
+
meta: {
|
|
6067
|
+
trackId: req.trackId,
|
|
6068
|
+
stepId: req.stepId,
|
|
6069
|
+
error: String(err)
|
|
6070
|
+
}
|
|
6071
|
+
});
|
|
6072
|
+
}
|
|
6073
|
+
}
|
|
6074
|
+
dev.scheduler.onResult(req.trackId, req.stepId, topScore, Date.now());
|
|
6075
|
+
}
|
|
6076
|
+
/**
|
|
6077
|
+
* Run the request once via the frameHandle, and — on a miss (null OR throw)
|
|
6078
|
+
* — retry ONCE with a `cropJpeg` fallback when one can be captured. Returns
|
|
6079
|
+
* the detail list, or `null` when both attempts fail to produce a result.
|
|
6080
|
+
*/
|
|
6081
|
+
async runOnce(deviceId, dev, req, frame) {
|
|
6082
|
+
const parent = {
|
|
6083
|
+
bbox: { ...frame.bbox },
|
|
6084
|
+
className: frame.className
|
|
6085
|
+
};
|
|
6086
|
+
if (frame.frameHandle !== void 0) try {
|
|
6087
|
+
const primary = await this.deps.runDetailSubtree({
|
|
6088
|
+
deviceId,
|
|
6089
|
+
frameHandle: frame.frameHandle,
|
|
6090
|
+
parent,
|
|
6091
|
+
steps: [req.stepId]
|
|
6092
|
+
}, frame.nodeId);
|
|
6093
|
+
if (primary !== null) return primary.details;
|
|
6094
|
+
} catch (err) {
|
|
6095
|
+
this.deps.logger.debug("detail primary call failed — trying crop fallback", {
|
|
6096
|
+
tags: { deviceId },
|
|
6097
|
+
meta: {
|
|
6098
|
+
trackId: req.trackId,
|
|
6099
|
+
stepId: req.stepId,
|
|
6100
|
+
error: String(err)
|
|
6101
|
+
}
|
|
6102
|
+
});
|
|
6103
|
+
}
|
|
6104
|
+
if (this.deps.captureCropBase64 !== void 0) try {
|
|
6105
|
+
const cropJpeg = await this.deps.captureCropBase64(frame);
|
|
6106
|
+
if (cropJpeg !== null) {
|
|
6107
|
+
const retry = await this.deps.runDetailSubtree({
|
|
6108
|
+
deviceId,
|
|
6109
|
+
cropJpeg,
|
|
6110
|
+
parent,
|
|
6111
|
+
steps: [req.stepId]
|
|
6112
|
+
}, frame.nodeId);
|
|
6113
|
+
if (retry !== null) return retry.details;
|
|
6114
|
+
}
|
|
6115
|
+
} catch (err) {
|
|
6116
|
+
this.deps.logger.debug("detail crop-fallback call failed", {
|
|
6117
|
+
tags: { deviceId },
|
|
6118
|
+
meta: {
|
|
6119
|
+
trackId: req.trackId,
|
|
6120
|
+
stepId: req.stepId,
|
|
6121
|
+
error: String(err)
|
|
6122
|
+
}
|
|
6123
|
+
});
|
|
6124
|
+
}
|
|
6125
|
+
this.warnFailThrottled(deviceId, dev, req);
|
|
6126
|
+
return null;
|
|
6127
|
+
}
|
|
6128
|
+
warnFailThrottled(deviceId, dev, req) {
|
|
6129
|
+
const now = Date.now();
|
|
6130
|
+
if (now - dev.lastFailWarnAt < FAIL_WARN_THROTTLE_MS) return;
|
|
6131
|
+
dev.lastFailWarnAt = now;
|
|
6132
|
+
this.deps.logger.warn("runDetailSubtree produced no result (frame + crop both missed)", {
|
|
6133
|
+
tags: { deviceId },
|
|
6134
|
+
meta: {
|
|
6135
|
+
trackId: req.trackId,
|
|
6136
|
+
stepId: req.stepId,
|
|
6137
|
+
reason: req.reason
|
|
6138
|
+
}
|
|
6139
|
+
});
|
|
6140
|
+
}
|
|
6141
|
+
};
|
|
6142
|
+
//#endregion
|
|
5313
6143
|
//#region src/pipeline-analytics/store/plate-store.ts
|
|
5314
6144
|
var PLATES_COLLECTION = "pipeline-analytics:plates";
|
|
5315
6145
|
var PLATE_COLUMNS = [
|
|
@@ -5625,6 +6455,38 @@ var PlateRecognizer = class {
|
|
|
5625
6455
|
}
|
|
5626
6456
|
};
|
|
5627
6457
|
//#endregion
|
|
6458
|
+
//#region src/shared/frame/shared-frame-resolver.ts
|
|
6459
|
+
function frameHandleKey(h) {
|
|
6460
|
+
return `${h.nodeId}:${h.shmId}:${h.slot}:${h.seq}`;
|
|
6461
|
+
}
|
|
6462
|
+
function createSharedFrameResolver(resolve) {
|
|
6463
|
+
let cache = null;
|
|
6464
|
+
return (handle) => {
|
|
6465
|
+
const key = frameHandleKey(handle);
|
|
6466
|
+
if (cache === null || cache.key !== key) cache = {
|
|
6467
|
+
key,
|
|
6468
|
+
value: resolve(handle)
|
|
6469
|
+
};
|
|
6470
|
+
return cache.value;
|
|
6471
|
+
};
|
|
6472
|
+
}
|
|
6473
|
+
//#endregion
|
|
6474
|
+
//#region src/shared/frame/encode-crop.ts
|
|
6475
|
+
/**
|
|
6476
|
+
* JPEG-encode an already-cropped raw RGB (24-bit) buffer. Used for the
|
|
6477
|
+
* NATIVE-resolution crop path: the decode worker returns the exact ROI pixels
|
|
6478
|
+
* at native res, so there is nothing left to crop — only to encode to the same
|
|
6479
|
+
* JPEG contract `extractCrop` produces (quality 90). Kept separate from
|
|
6480
|
+
* `extractCrop` (which crops a FULL frame) so the native path never re-crops.
|
|
6481
|
+
*/
|
|
6482
|
+
async function encodeRgbCropToJpeg(bytes, width, height) {
|
|
6483
|
+
return (0, sharp.default)(Buffer.from(bytes), { raw: {
|
|
6484
|
+
width,
|
|
6485
|
+
height,
|
|
6486
|
+
channels: 3
|
|
6487
|
+
} }).jpeg({ quality: 90 }).toBuffer();
|
|
6488
|
+
}
|
|
6489
|
+
//#endregion
|
|
5628
6490
|
//#region src/pipeline-analytics/pipeline/event-child-crops.ts
|
|
5629
6491
|
/**
|
|
5630
6492
|
* Compute Intersection-over-Union between two (x,y,w,h) bounding boxes.
|
|
@@ -5922,6 +6784,12 @@ function createEventMediaHandler(deps) {
|
|
|
5922
6784
|
* surface to turn the refinement pipeline on/off for a camera.
|
|
5923
6785
|
*/
|
|
5924
6786
|
var TTL_SWEEP_INTERVAL_MS = 5e3;
|
|
6787
|
+
/** Padding applied to the parent bbox for the `cropJpeg` retry fallback when a
|
|
6788
|
+
* scheduled detail call's frameHandle lease is already gone. */
|
|
6789
|
+
var DETAIL_FALLBACK_CROP_PADDING = .15;
|
|
6790
|
+
/** How long the active CLIP model id (from the embedding-encoder) is cached
|
|
6791
|
+
* before re-reading. */
|
|
6792
|
+
var CLIP_MODEL_ID_CACHE_TTL_MS = 6e4;
|
|
5925
6793
|
var SETTINGS_CACHE_TTL_MS = 5e3;
|
|
5926
6794
|
/** §5 best-frame: a track's `thumbnail` is overwritten only when the current
|
|
5927
6795
|
* detection confidence beats the held best by at least this margin (hysteresis
|
|
@@ -5929,6 +6797,18 @@ var SETTINGS_CACHE_TTL_MS = 5e3;
|
|
|
5929
6797
|
var BEST_FRAME_HYSTERESIS = .05;
|
|
5930
6798
|
/** §5 best-frame: at most one best-thumbnail capture per this interval per track. */
|
|
5931
6799
|
var BEST_FRAME_MIN_GAP_MS = 2e3;
|
|
6800
|
+
/** Design B: cap (px) on the width of the native KEY FRAME (full-frame native
|
|
6801
|
+
* capture). Native resolution is the point, but a full 4K RGB surface over the
|
|
6802
|
+
* transport per new-best is wasteful for a web detail view — 1920px keeps a
|
|
6803
|
+
* sharp native frame while bounding the copy (a miss falls back to the
|
|
6804
|
+
* detection-res frame, which is already ≤640px). */
|
|
6805
|
+
var KEYFRAME_NATIVE_MAX_WIDTH = 1920;
|
|
6806
|
+
/** getKeyEvents: max completed tracks pulled from a window before importance
|
|
6807
|
+
* ranking. Ordering is by importance (not firstSeen) and legacy rows score on
|
|
6808
|
+
* read, so we over-fetch candidates and trim to `limit` after sorting. */
|
|
6809
|
+
var KEY_EVENT_CANDIDATE_CAP = 500;
|
|
6810
|
+
/** getKeyEvents: default page size when the caller omits `limit`. */
|
|
6811
|
+
var KEY_EVENT_DEFAULT_LIMIT = 50;
|
|
5932
6812
|
/** Cluster setting key (in the centralized addon store) selecting the SINGLE
|
|
5933
6813
|
* node that runs post-analysis (event/media/audio/motion generation). All
|
|
5934
6814
|
* other nodes are fully inert. No multi-node balancing. Default: the hub. */
|
|
@@ -5942,6 +6822,15 @@ var RETENTION_SWEEP_INTERVAL_MS = 5 * 6e4;
|
|
|
5942
6822
|
var AUDIO_EVENT_HEARTBEAT_MS = 5e3;
|
|
5943
6823
|
var MOTION_EVENT_HEARTBEAT_MS = 5e3;
|
|
5944
6824
|
/**
|
|
6825
|
+
* Decode a base64 little-endian float32 vector (the `DetailResult.embedding`
|
|
6826
|
+
* wire encoding produced by `runDetailSubtree`) back into a plain number[].
|
|
6827
|
+
*/
|
|
6828
|
+
function decodeEmbeddingBase64(base64) {
|
|
6829
|
+
const bytes = Buffer.from(base64, "base64");
|
|
6830
|
+
const view = new Float32Array(bytes.buffer, bytes.byteOffset, Math.floor(bytes.byteLength / 4));
|
|
6831
|
+
return Array.from(view);
|
|
6832
|
+
}
|
|
6833
|
+
/**
|
|
5945
6834
|
* Re-home the global analytics sections into the per-device `Analytics`
|
|
5946
6835
|
* top-tab. Every section defaults to the `Analytics` tab (so the
|
|
5947
6836
|
* device-manager aggregator groups them) AND is marked
|
|
@@ -5991,6 +6880,14 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
5991
6880
|
plateStore = null;
|
|
5992
6881
|
plateRecognizer = null;
|
|
5993
6882
|
objectEmbeddingStore = null;
|
|
6883
|
+
/** Two-plane detail scheduler/dispatcher: per-track on-demand face/clip/
|
|
6884
|
+
* classifier/plate enrichment via `pipelineRunner.runDetailSubtree`. Replaces
|
|
6885
|
+
* the per-frame child consumption the executor no longer emits. */
|
|
6886
|
+
detailDispatcher = null;
|
|
6887
|
+
/** Active CLIP image-embedding model id (== the text encoder's), TTL-cached.
|
|
6888
|
+
* Stamped on object-embedding rows from the detail plane so semantic search's
|
|
6889
|
+
* same-model gate keeps matching. */
|
|
6890
|
+
clipModelIdCache = null;
|
|
5994
6891
|
/** Frame-based event/track media (crop + boxed full-frame) from the
|
|
5995
6892
|
* detection-pipeline DECODED frame — the ONLY image source (never the
|
|
5996
6893
|
* snapshot cap). Null when shm frame access is unavailable. */
|
|
@@ -6048,6 +6945,23 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6048
6945
|
hysteresis: BEST_FRAME_HYSTERESIS,
|
|
6049
6946
|
minGapMs: BEST_FRAME_MIN_GAP_MS
|
|
6050
6947
|
});
|
|
6948
|
+
/** Best (highest-confidence) CLIP-object detection per track — drives ONE
|
|
6949
|
+
* tight object-crop capture whose media key is written onto the embedding
|
|
6950
|
+
* row (so a semantic-search hit's thumbnail IS the embedded crop). Shares the
|
|
6951
|
+
* unified best-per-track decision (`TrackBestSelector`); the persistent
|
|
6952
|
+
* cross-restart "best" gate stays in `ObjectEmbeddingStore.upsertIfBetter`. */
|
|
6953
|
+
objectEmbeddingBestSelector = new TrackBestSelector();
|
|
6954
|
+
/** Design B: the track's shared native key-frame media key, captured at the
|
|
6955
|
+
* best-detection moment (object-embedding best path). Read by the face path
|
|
6956
|
+
* at track end so a face row links to the SAME single key frame. Cleared on
|
|
6957
|
+
* track end. */
|
|
6958
|
+
keyFrameKeyByTrackId = /* @__PURE__ */ new Map();
|
|
6959
|
+
/** The shared crop extractor (native-res first, detection-frame fallback),
|
|
6960
|
+
* captured in the constructor so `processFrame` can crop object thumbnails in
|
|
6961
|
+
* the same live-frame window as the face/plate/event-media captures. The
|
|
6962
|
+
* optional `maxWidth` caps the native crop width (used for the full-frame key
|
|
6963
|
+
* frame so a 4K native surface never floods the transport). */
|
|
6964
|
+
captureCrop = null;
|
|
6051
6965
|
shuttingDown = false;
|
|
6052
6966
|
/** True only on the cluster's designated post-processing node. When false the
|
|
6053
6967
|
* addon subscribes to NOTHING — fully inert (no event/media generation). */
|
|
@@ -6081,7 +6995,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6081
6995
|
let storage = this.ctx.kernel.storage;
|
|
6082
6996
|
const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
|
|
6083
6997
|
if (mediaRoot) {
|
|
6084
|
-
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-
|
|
6998
|
+
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-BN31iDiA.js"));
|
|
6085
6999
|
storage = new FilesystemStorageProvider(mediaRoot);
|
|
6086
7000
|
logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
|
|
6087
7001
|
}
|
|
@@ -6147,23 +7061,62 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6147
7061
|
});
|
|
6148
7062
|
const ownNodeIdForFaces = ownNodeId;
|
|
6149
7063
|
const frameReadersForFaces = this.frameReaders;
|
|
6150
|
-
const
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
7064
|
+
const pipelineRunnerApi = api.pipelineRunner;
|
|
7065
|
+
const cropMetricLogger = logger.child("NativeCrop");
|
|
7066
|
+
let nativeHits = 0;
|
|
7067
|
+
let nativeFallbacks = 0;
|
|
7068
|
+
let lastCropMetricAt = 0;
|
|
7069
|
+
const NATIVE_CROP_METRIC_INTERVAL_MS = 3e4;
|
|
7070
|
+
const bumpCropMetric = (hit) => {
|
|
7071
|
+
if (hit) nativeHits += 1;
|
|
7072
|
+
else nativeFallbacks += 1;
|
|
7073
|
+
const now = Date.now();
|
|
7074
|
+
if (now - lastCropMetricAt < NATIVE_CROP_METRIC_INTERVAL_MS) return;
|
|
7075
|
+
lastCropMetricAt = now;
|
|
7076
|
+
cropMetricLogger.info("native-crop window", { meta: {
|
|
7077
|
+
nativeHits,
|
|
7078
|
+
detectionFrameFallbacks: nativeFallbacks
|
|
7079
|
+
} });
|
|
7080
|
+
};
|
|
7081
|
+
const tryNativeCrop = async (frameHandle, paddedNorm, maxWidth) => {
|
|
7082
|
+
if (!pipelineRunnerApi?.getNativeCrop) return null;
|
|
7083
|
+
try {
|
|
7084
|
+
const native = await pipelineRunnerApi.getNativeCrop.query({
|
|
7085
|
+
handle: frameHandle,
|
|
7086
|
+
bbox: paddedNorm,
|
|
7087
|
+
...maxWidth !== void 0 ? { maxWidth } : {}
|
|
7088
|
+
}, require_dist.nodePin(frameHandle.nodeId));
|
|
7089
|
+
if (!native || native.width <= 0 || native.height <= 0) return null;
|
|
7090
|
+
return await encodeRgbCropToJpeg(Buffer.from(native.bytes), native.width, native.height);
|
|
7091
|
+
} catch (err) {
|
|
7092
|
+
cropMetricLogger.debug("native crop request failed — falling back", { meta: { error: require_dist.errMsg(err) } });
|
|
7093
|
+
return null;
|
|
7094
|
+
}
|
|
7095
|
+
};
|
|
7096
|
+
const resolveFrameShared = createSharedFrameResolver((frameHandle) => require_resolve_frame.resolveFrame(frameHandle, {
|
|
7097
|
+
ownNodeId: ownNodeIdForFaces,
|
|
7098
|
+
readers: frameReadersForFaces,
|
|
7099
|
+
getRemoteFrame
|
|
7100
|
+
}));
|
|
7101
|
+
const captureCrop = async (frameHandle, bbox, frameWidth, frameHeight, padding, maxWidth) => {
|
|
6158
7102
|
const paddedNorm = padBbox({
|
|
6159
7103
|
x: bbox.x / frameWidth,
|
|
6160
7104
|
y: bbox.y / frameHeight,
|
|
6161
7105
|
w: bbox.w / frameWidth,
|
|
6162
7106
|
h: bbox.h / frameHeight
|
|
6163
7107
|
}, padding);
|
|
6164
|
-
const
|
|
7108
|
+
const nativeCrop = await tryNativeCrop(frameHandle, paddedNorm, maxWidth);
|
|
7109
|
+
if (nativeCrop) {
|
|
7110
|
+
bumpCropMetric(true);
|
|
7111
|
+
return nativeCrop;
|
|
7112
|
+
}
|
|
7113
|
+
bumpCropMetric(false);
|
|
7114
|
+
const decoded = await resolveFrameShared(frameHandle);
|
|
7115
|
+
if (!decoded || decoded.format !== "rgb") return null;
|
|
7116
|
+
const { crop } = await require_resolve_frame.extractCrop(Buffer.isBuffer(decoded.data) ? decoded.data : Buffer.from(decoded.data), decoded.width, decoded.height, paddedNorm);
|
|
6165
7117
|
return crop;
|
|
6166
7118
|
};
|
|
7119
|
+
this.captureCrop = captureCrop;
|
|
6167
7120
|
this.faceRecognizer = new FaceRecognizer({
|
|
6168
7121
|
identityStore: this.identityStore,
|
|
6169
7122
|
faceStore: this.faceStore,
|
|
@@ -6171,6 +7124,16 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6171
7124
|
trackStore: this.trackStore,
|
|
6172
7125
|
eventStore: this.eventStore,
|
|
6173
7126
|
captureCrop,
|
|
7127
|
+
recomputeImportance: (trackId) => {
|
|
7128
|
+
const trackStore = this.trackStore;
|
|
7129
|
+
const eventStore = this.eventStore;
|
|
7130
|
+
if (!trackStore || !eventStore) return Promise.resolve();
|
|
7131
|
+
return recomputeTrackImportance({
|
|
7132
|
+
trackStore,
|
|
7133
|
+
eventStore
|
|
7134
|
+
}, trackId);
|
|
7135
|
+
},
|
|
7136
|
+
getKeyFrameMediaKey: (trackId) => this.keyFrameKeyByTrackId.get(trackId),
|
|
6174
7137
|
logger: logger.child("FaceRecognizer")
|
|
6175
7138
|
});
|
|
6176
7139
|
this.faceRecognizer.refreshGallery();
|
|
@@ -6184,6 +7147,21 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6184
7147
|
store: api.settingsStore,
|
|
6185
7148
|
logger: logger.child("ObjectEmbeddingStore")
|
|
6186
7149
|
});
|
|
7150
|
+
const runnerApi = api.pipelineRunner;
|
|
7151
|
+
this.detailDispatcher = new TrackDetailDispatcher({
|
|
7152
|
+
logger: logger.child("DetailDispatcher"),
|
|
7153
|
+
runDetailSubtree: async (input, nodeId) => {
|
|
7154
|
+
if (!runnerApi?.runDetailSubtree) return null;
|
|
7155
|
+
if (nodeId !== void 0) return runnerApi.runDetailSubtree.mutate(input, require_dist.nodePin(nodeId));
|
|
7156
|
+
return runnerApi.runDetailSubtree.mutate(input);
|
|
7157
|
+
},
|
|
7158
|
+
routeResults: (deviceId, trackId, details, frame) => this.routeDetailResults(deviceId, trackId, details, frame),
|
|
7159
|
+
captureCropBase64: async (frame) => {
|
|
7160
|
+
if (frame.frameHandle === void 0 || !this.captureCrop) return null;
|
|
7161
|
+
const buf = await this.captureCrop(frame.frameHandle, { ...frame.bbox }, frame.frameWidth, frame.frameHeight, DETAIL_FALLBACK_CROP_PADDING);
|
|
7162
|
+
return buf ? buf.toString("base64") : null;
|
|
7163
|
+
}
|
|
7164
|
+
});
|
|
6187
7165
|
this.bindingCache = new BindingCache({
|
|
6188
7166
|
api,
|
|
6189
7167
|
logger: logger.child("BindingCache")
|
|
@@ -6208,7 +7186,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6208
7186
|
try {
|
|
6209
7187
|
const handler = createEventMediaHandler({ getMedia: async (id) => {
|
|
6210
7188
|
try {
|
|
6211
|
-
return await this.
|
|
7189
|
+
return await this.readMediaByEventOrKey(id);
|
|
6212
7190
|
} catch (err) {
|
|
6213
7191
|
this.ctx.logger.warn("readEventThumbnail failed", { meta: {
|
|
6214
7192
|
eventId: id,
|
|
@@ -6588,10 +7566,13 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6588
7566
|
}
|
|
6589
7567
|
this.zoneAnalytics?.destroy();
|
|
6590
7568
|
this.audioMetrics?.destroy();
|
|
7569
|
+
this.detailDispatcher?.dispose();
|
|
7570
|
+
this.detailDispatcher = null;
|
|
6591
7571
|
this.processors.clear();
|
|
6592
7572
|
this.lastActiveTrackIds.clear();
|
|
6593
7573
|
this.dropoutSkipsByKey.clear();
|
|
6594
7574
|
this.bestFrameTracker.clear();
|
|
7575
|
+
this.objectEmbeddingBestSelector.clear();
|
|
6595
7576
|
this.levelStateByDevice.clear();
|
|
6596
7577
|
this.settingsCacheByDevice.clear();
|
|
6597
7578
|
this.sensitivityCacheByDevice.clear();
|
|
@@ -6611,7 +7592,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6611
7592
|
async handleInferenceResult(data) {
|
|
6612
7593
|
if (this.shuttingDown) return;
|
|
6613
7594
|
const { deviceId, frame } = data;
|
|
6614
|
-
await this.processFrame(deviceId, frame, "pipeline", data.frameHandle);
|
|
7595
|
+
await this.processFrame(deviceId, frame, "pipeline", data.frameHandle, data.detailSteps);
|
|
6615
7596
|
}
|
|
6616
7597
|
/**
|
|
6617
7598
|
* Run one detection frame through the analysis layers for a given
|
|
@@ -6621,7 +7602,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6621
7602
|
* tracking/zone/event state never crosses between sources. Emits the
|
|
6622
7603
|
* SAME canonical events, distinguished only by `source`.
|
|
6623
7604
|
*/
|
|
6624
|
-
async processFrame(deviceId, frame, source, frameHandle) {
|
|
7605
|
+
async processFrame(deviceId, frame, source, frameHandle, detailSteps) {
|
|
6625
7606
|
if (this.shuttingDown) return;
|
|
6626
7607
|
if (!await this.bindingCache.isActive(deviceId)) return;
|
|
6627
7608
|
const key = this.procKey(deviceId, source);
|
|
@@ -6733,6 +7714,26 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6733
7714
|
} });
|
|
6734
7715
|
}
|
|
6735
7716
|
this.lastActiveTrackIds.set(key, currentTrackIds);
|
|
7717
|
+
if (this.detailDispatcher && detailSteps && detailSteps.length > 0) {
|
|
7718
|
+
const dispatcher = this.detailDispatcher;
|
|
7719
|
+
const steps = detailSteps;
|
|
7720
|
+
for (const t of result.tracked) {
|
|
7721
|
+
const detailFrame = {
|
|
7722
|
+
bbox: { ...t.bbox },
|
|
7723
|
+
frameWidth: result.frameWidth,
|
|
7724
|
+
frameHeight: result.frameHeight,
|
|
7725
|
+
className: t.className,
|
|
7726
|
+
confidence: t.confidence,
|
|
7727
|
+
timestamp: result.timestamp,
|
|
7728
|
+
...frameHandle !== void 0 ? {
|
|
7729
|
+
frameHandle,
|
|
7730
|
+
nodeId: frameHandle.nodeId
|
|
7731
|
+
} : {}
|
|
7732
|
+
};
|
|
7733
|
+
if (prevIds.has(t.trackId)) dispatcher.onFrame(deviceId, t.trackId, steps, detailFrame, result.timestamp);
|
|
7734
|
+
else dispatcher.onTrackStarted(deviceId, t.trackId, t.className, steps, detailFrame, result.timestamp);
|
|
7735
|
+
}
|
|
7736
|
+
}
|
|
6736
7737
|
if (newTrackCount > 0 || lostTrackCount > 0 || result.objectEvents.length > 0) {
|
|
6737
7738
|
const byState = {};
|
|
6738
7739
|
for (const t of result.tracked) byState[t.state] = (byState[t.state] ?? 0) + 1;
|
|
@@ -6747,16 +7748,18 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6747
7748
|
} });
|
|
6748
7749
|
}
|
|
6749
7750
|
await Promise.all(result.objectEvents.map((e) => this.eventStore.insertObject(e)));
|
|
6750
|
-
|
|
6751
|
-
|
|
7751
|
+
const objectEmbeddingBests = [];
|
|
7752
|
+
if (this.objectEmbeddingStore) for (const t of result.tracked) {
|
|
7753
|
+
if (!isClipObjectEmbedding(t)) continue;
|
|
7754
|
+
if (this.objectEmbeddingBestSelector.observe({
|
|
6752
7755
|
trackId: t.trackId,
|
|
6753
|
-
|
|
6754
|
-
|
|
7756
|
+
confidence: t.confidence,
|
|
7757
|
+
atMs: result.timestamp,
|
|
6755
7758
|
className: t.className,
|
|
7759
|
+
bbox: t.bbox,
|
|
6756
7760
|
embedding: t.embedding,
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
});
|
|
7761
|
+
embeddingModelId: t.embeddingModelId
|
|
7762
|
+
})) objectEmbeddingBests.push(t);
|
|
6760
7763
|
}
|
|
6761
7764
|
const faceSettings = this.faceRecognizer ? await this.resolveDeviceFaceSettings(deviceId) : null;
|
|
6762
7765
|
const faceGloballyEnabled = this.faceRecognizer ? await this.resolveGlobalFaceEnabled() : false;
|
|
@@ -6829,6 +7832,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6829
7832
|
cropPadding: mediaSettings.cropPadding,
|
|
6830
7833
|
...frameHandle !== void 0 ? { frameHandle } : {}
|
|
6831
7834
|
});
|
|
7835
|
+
if (objectEmbeddingBests.length > 0) this.persistObjectEmbeddingBests(deviceId, result.timestamp, objectEmbeddingBests, frameHandle, result.frameWidth, result.frameHeight, mediaSettings.cropPadding);
|
|
6832
7836
|
for (const e of result.objectEvents) this.ctx.eventBus.emit({
|
|
6833
7837
|
id: `pa-${e.id}`,
|
|
6834
7838
|
timestamp: new Date(e.timestamp),
|
|
@@ -6941,6 +7945,142 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6941
7945
|
return settings;
|
|
6942
7946
|
}
|
|
6943
7947
|
/**
|
|
7948
|
+
* Route one track's `runDetailSubtree` results (two-plane detail dispatch)
|
|
7949
|
+
* into the EXISTING per-track consumers, discriminated by payload SHAPE:
|
|
7950
|
+
* • embedding + alignedCropJpeg → FACE (arcface): the FaceRecognizer's
|
|
7951
|
+
* candidate/best-face/gallery/keyFrame path (input source cut-over);
|
|
7952
|
+
* • embedding only → CLIP object embedding → the object-embedding store
|
|
7953
|
+
* (semantic search);
|
|
7954
|
+
* • label only → classifier answer / plate OCR text → the track's
|
|
7955
|
+
* enrichment label the notifier/UI already read.
|
|
7956
|
+
* Best-effort (D8): a per-detail failure is logged and never propagated.
|
|
7957
|
+
*/
|
|
7958
|
+
async routeDetailResults(deviceId, trackId, details, frame) {
|
|
7959
|
+
for (const d of details) try {
|
|
7960
|
+
if (d.className === "face" && d.embedding !== void 0) await this.routeFaceDetail(deviceId, trackId, d, frame);
|
|
7961
|
+
else if (d.embedding !== void 0) await this.routeClipDetail(deviceId, trackId, d, frame.timestamp);
|
|
7962
|
+
else if (d.label !== void 0 && d.label.length > 0) await this.applyTrackEnrichmentLabel(deviceId, trackId, d.label);
|
|
7963
|
+
} catch (err) {
|
|
7964
|
+
this.ctx.logger.warn("detail result route failed", {
|
|
7965
|
+
tags: { deviceId },
|
|
7966
|
+
meta: {
|
|
7967
|
+
trackId,
|
|
7968
|
+
stepId: d.stepId,
|
|
7969
|
+
error: require_dist.errMsg(err)
|
|
7970
|
+
}
|
|
7971
|
+
});
|
|
7972
|
+
}
|
|
7973
|
+
}
|
|
7974
|
+
/** Face-embedding detail → the FaceRecognizer (same gate + logic as the
|
|
7975
|
+
* former per-frame face path; only the input source moved). */
|
|
7976
|
+
async routeFaceDetail(deviceId, trackId, detail, frame) {
|
|
7977
|
+
if (!this.faceRecognizer || detail.embedding === void 0) return;
|
|
7978
|
+
if (!await this.resolveGlobalFaceEnabled()) return;
|
|
7979
|
+
const [settings, media] = await Promise.all([this.resolveDeviceFaceSettings(deviceId), this.resolveDeviceMediaSettings(deviceId)]);
|
|
7980
|
+
await this.faceRecognizer.ingestFaceDetail({
|
|
7981
|
+
deviceId,
|
|
7982
|
+
trackId,
|
|
7983
|
+
timestamp: frame.timestamp,
|
|
7984
|
+
frameWidth: frame.frameWidth,
|
|
7985
|
+
frameHeight: frame.frameHeight,
|
|
7986
|
+
score: detail.score,
|
|
7987
|
+
embedding: decodeEmbeddingBase64(detail.embedding),
|
|
7988
|
+
parentBbox: { ...frame.bbox },
|
|
7989
|
+
...detail.bbox !== void 0 ? { faceBbox: { ...detail.bbox } } : {},
|
|
7990
|
+
...detail.alignedCropJpeg !== void 0 ? { alignedCropJpeg: detail.alignedCropJpeg } : {},
|
|
7991
|
+
settings,
|
|
7992
|
+
cropPadding: media.cropPadding,
|
|
7993
|
+
...frame.frameHandle !== void 0 ? { frameHandle: frame.frameHandle } : {}
|
|
7994
|
+
});
|
|
7995
|
+
}
|
|
7996
|
+
/** CLIP object-embedding detail → the object-embedding store (semantic
|
|
7997
|
+
* search). Stamps the active encoder's model id so the same-model search
|
|
7998
|
+
* gate keeps matching. */
|
|
7999
|
+
async routeClipDetail(deviceId, trackId, detail, timestamp) {
|
|
8000
|
+
const store = this.objectEmbeddingStore;
|
|
8001
|
+
if (!store || detail.embedding === void 0) return;
|
|
8002
|
+
const modelId = await this.resolveClipModelId();
|
|
8003
|
+
if (modelId === null) {
|
|
8004
|
+
this.ctx.logger.debug("clip detail dropped — no active embedding model id", {
|
|
8005
|
+
tags: { deviceId },
|
|
8006
|
+
meta: {
|
|
8007
|
+
trackId,
|
|
8008
|
+
stepId: detail.stepId
|
|
8009
|
+
}
|
|
8010
|
+
});
|
|
8011
|
+
return;
|
|
8012
|
+
}
|
|
8013
|
+
await store.upsertIfBetter({
|
|
8014
|
+
trackId,
|
|
8015
|
+
deviceId,
|
|
8016
|
+
timestamp,
|
|
8017
|
+
className: detail.className,
|
|
8018
|
+
embedding: decodeEmbeddingBase64(detail.embedding),
|
|
8019
|
+
modelId,
|
|
8020
|
+
confidence: detail.score
|
|
8021
|
+
});
|
|
8022
|
+
}
|
|
8023
|
+
/** Classifier answer / plate OCR text → the track's enrichment label
|
|
8024
|
+
* (TrackStore + persisted events + importance), mirroring the FaceRecognizer
|
|
8025
|
+
* label-propagation path. */
|
|
8026
|
+
async applyTrackEnrichmentLabel(deviceId, trackId, label) {
|
|
8027
|
+
try {
|
|
8028
|
+
await this.trackStore?.setLabel(trackId, label);
|
|
8029
|
+
} catch (err) {
|
|
8030
|
+
this.ctx.logger.warn("detail label setLabel failed", {
|
|
8031
|
+
tags: { deviceId },
|
|
8032
|
+
meta: {
|
|
8033
|
+
trackId,
|
|
8034
|
+
error: require_dist.errMsg(err)
|
|
8035
|
+
}
|
|
8036
|
+
});
|
|
8037
|
+
}
|
|
8038
|
+
try {
|
|
8039
|
+
await this.eventStore?.setLabelForTrack(trackId, label);
|
|
8040
|
+
} catch (err) {
|
|
8041
|
+
this.ctx.logger.warn("detail label setLabelForTrack failed", {
|
|
8042
|
+
tags: { deviceId },
|
|
8043
|
+
meta: {
|
|
8044
|
+
trackId,
|
|
8045
|
+
error: require_dist.errMsg(err)
|
|
8046
|
+
}
|
|
8047
|
+
});
|
|
8048
|
+
}
|
|
8049
|
+
try {
|
|
8050
|
+
const trackStore = this.trackStore;
|
|
8051
|
+
const eventStore = this.eventStore;
|
|
8052
|
+
if (trackStore && eventStore) await recomputeTrackImportance({
|
|
8053
|
+
trackStore,
|
|
8054
|
+
eventStore
|
|
8055
|
+
}, trackId);
|
|
8056
|
+
} catch (err) {
|
|
8057
|
+
this.ctx.logger.debug("detail label recomputeImportance failed", {
|
|
8058
|
+
tags: { deviceId },
|
|
8059
|
+
meta: {
|
|
8060
|
+
trackId,
|
|
8061
|
+
error: require_dist.errMsg(err)
|
|
8062
|
+
}
|
|
8063
|
+
});
|
|
8064
|
+
}
|
|
8065
|
+
}
|
|
8066
|
+
/** Active CLIP image-embedding model id (== the text encoder's), TTL-cached.
|
|
8067
|
+
* Returns null when the embedding-encoder cap is unavailable. */
|
|
8068
|
+
async resolveClipModelId() {
|
|
8069
|
+
const now = Date.now();
|
|
8070
|
+
if (this.clipModelIdCache && now < this.clipModelIdCache.expiresAt) return this.clipModelIdCache.value;
|
|
8071
|
+
let value = null;
|
|
8072
|
+
try {
|
|
8073
|
+
value = (await this.ctx.api.embeddingEncoder.getInfo.query())?.modelId ?? null;
|
|
8074
|
+
} catch (err) {
|
|
8075
|
+
this.ctx.logger.debug("resolveClipModelId: getInfo failed", { meta: { error: require_dist.errMsg(err) } });
|
|
8076
|
+
}
|
|
8077
|
+
this.clipModelIdCache = {
|
|
8078
|
+
value,
|
|
8079
|
+
expiresAt: now + CLIP_MODEL_ID_CACHE_TTL_MS
|
|
8080
|
+
};
|
|
8081
|
+
return value;
|
|
8082
|
+
}
|
|
8083
|
+
/**
|
|
6944
8084
|
* §5 — decide which active tracks need periodic media THIS frame. Pure over
|
|
6945
8085
|
* TrackStore.lastSnapshotAt + the per-track best-confidence map:
|
|
6946
8086
|
* • `snapshot` (append) + `lastFrame` (rolling overwrite) fire together on
|
|
@@ -6950,6 +8090,83 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6950
8090
|
* NOT gated by saveThumbnails (a single best still-frame is always useful).
|
|
6951
8091
|
* The per-track best-confidence map is updated here as a side effect.
|
|
6952
8092
|
*/
|
|
8093
|
+
/**
|
|
8094
|
+
* Capture the tight object crop for each new-best CLIP track (native-res via
|
|
8095
|
+
* the shared extractor) and upsert the embedding row with that crop's media
|
|
8096
|
+
* key. The crop uses `putReplacing` so exactly ONE object crop is kept per
|
|
8097
|
+
* track (the current peak). The embedding is upserted even when no crop was
|
|
8098
|
+
* captured (no frame handle) so semantic search still works — the crop just
|
|
8099
|
+
* enhances the search-hit thumbnail. `upsertIfBetter` remains the durable
|
|
8100
|
+
* cross-restart best gate (R3). Best-effort; issued in the live-frame window.
|
|
8101
|
+
*/
|
|
8102
|
+
async persistObjectEmbeddingBests(deviceId, timestamp, bests, frameHandle, frameWidth, frameHeight, cropPadding) {
|
|
8103
|
+
const store = this.objectEmbeddingStore;
|
|
8104
|
+
if (!store) return;
|
|
8105
|
+
await Promise.all(bests.map(async (t) => {
|
|
8106
|
+
if (!isClipObjectEmbedding(t)) return;
|
|
8107
|
+
let mediaKey;
|
|
8108
|
+
let keyFrameMediaKey;
|
|
8109
|
+
if (frameHandle !== void 0 && this.captureCrop && this.mediaStore) {
|
|
8110
|
+
try {
|
|
8111
|
+
const crop = await this.captureCrop(frameHandle, t.bbox, frameWidth, frameHeight, cropPadding);
|
|
8112
|
+
if (crop) mediaKey = await this.mediaStore.putReplacing({
|
|
8113
|
+
deviceId,
|
|
8114
|
+
ownerKind: "track",
|
|
8115
|
+
ownerId: t.trackId,
|
|
8116
|
+
kind: "crop",
|
|
8117
|
+
timestamp,
|
|
8118
|
+
data: crop
|
|
8119
|
+
});
|
|
8120
|
+
} catch (err) {
|
|
8121
|
+
this.ctx.logger.debug("object-embedding crop capture failed", {
|
|
8122
|
+
tags: { deviceId },
|
|
8123
|
+
meta: {
|
|
8124
|
+
trackId: t.trackId,
|
|
8125
|
+
error: require_dist.errMsg(err)
|
|
8126
|
+
}
|
|
8127
|
+
});
|
|
8128
|
+
}
|
|
8129
|
+
try {
|
|
8130
|
+
const keyFrame = await this.captureCrop(frameHandle, {
|
|
8131
|
+
x: 0,
|
|
8132
|
+
y: 0,
|
|
8133
|
+
w: frameWidth,
|
|
8134
|
+
h: frameHeight
|
|
8135
|
+
}, frameWidth, frameHeight, 0, KEYFRAME_NATIVE_MAX_WIDTH);
|
|
8136
|
+
if (keyFrame) {
|
|
8137
|
+
keyFrameMediaKey = await this.mediaStore.putReplacing({
|
|
8138
|
+
deviceId,
|
|
8139
|
+
ownerKind: "track",
|
|
8140
|
+
ownerId: t.trackId,
|
|
8141
|
+
kind: "keyFrame",
|
|
8142
|
+
timestamp,
|
|
8143
|
+
data: keyFrame
|
|
8144
|
+
});
|
|
8145
|
+
this.keyFrameKeyByTrackId.set(t.trackId, keyFrameMediaKey);
|
|
8146
|
+
}
|
|
8147
|
+
} catch (err) {
|
|
8148
|
+
this.ctx.logger.debug("key-frame capture failed", {
|
|
8149
|
+
tags: { deviceId },
|
|
8150
|
+
meta: {
|
|
8151
|
+
trackId: t.trackId,
|
|
8152
|
+
error: require_dist.errMsg(err)
|
|
8153
|
+
}
|
|
8154
|
+
});
|
|
8155
|
+
}
|
|
8156
|
+
}
|
|
8157
|
+
await store.upsertIfBetter({
|
|
8158
|
+
trackId: t.trackId,
|
|
8159
|
+
deviceId,
|
|
8160
|
+
timestamp,
|
|
8161
|
+
className: t.className,
|
|
8162
|
+
embedding: t.embedding,
|
|
8163
|
+
modelId: t.embeddingModelId,
|
|
8164
|
+
confidence: t.confidence,
|
|
8165
|
+
...mediaKey !== void 0 ? { mediaKey } : {},
|
|
8166
|
+
...keyFrameMediaKey !== void 0 ? { keyFrameMediaKey } : {}
|
|
8167
|
+
});
|
|
8168
|
+
}));
|
|
8169
|
+
}
|
|
6953
8170
|
buildSnapshotTargets(tracked, timestamp, media) {
|
|
6954
8171
|
const targets = [];
|
|
6955
8172
|
for (const t of tracked) {
|
|
@@ -7221,9 +8438,37 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
7221
8438
|
positions: t.positions.length
|
|
7222
8439
|
}
|
|
7223
8440
|
});
|
|
7224
|
-
this.faceRecognizer?.onTrackEnd(t.deviceId, t.trackId);
|
|
8441
|
+
const faceEnd = this.faceRecognizer?.onTrackEnd(t.deviceId, t.trackId);
|
|
8442
|
+
const dropKeyFrameKey = () => {
|
|
8443
|
+
this.keyFrameKeyByTrackId.delete(t.trackId);
|
|
8444
|
+
};
|
|
8445
|
+
if (faceEnd) faceEnd.finally(dropKeyFrameKey);
|
|
8446
|
+
else dropKeyFrameKey();
|
|
7225
8447
|
this.plateRecognizer?.onTrackEnd(t.deviceId, t.trackId);
|
|
8448
|
+
try {
|
|
8449
|
+
const peak = await this.eventStore?.peakForTrack(t.trackId);
|
|
8450
|
+
if (peak) {
|
|
8451
|
+
const { importance, reason } = computeImportance({
|
|
8452
|
+
peakConfidence: this.bestFrameTracker.peak(t.trackId)?.confidence ?? peak.peakConfidence,
|
|
8453
|
+
className: t.className,
|
|
8454
|
+
durationMs: duration,
|
|
8455
|
+
peakBboxAreaFrac: peak.peakBboxAreaFrac,
|
|
8456
|
+
totalDistance: t.totalDistance,
|
|
8457
|
+
zonesVisited: t.zonesVisited,
|
|
8458
|
+
...t.label !== void 0 ? { label: t.label } : {}
|
|
8459
|
+
});
|
|
8460
|
+
await this.trackStore?.setImportance(t.trackId, importance, reason, peak.bestEventId);
|
|
8461
|
+
if (peak.bestEventId !== void 0) await this.eventStore?.setImportanceForTrack(t.trackId, importance);
|
|
8462
|
+
}
|
|
8463
|
+
} catch (err) {
|
|
8464
|
+
this.ctx.logger.debug("importance scoring failed", { meta: {
|
|
8465
|
+
trackId: t.trackId,
|
|
8466
|
+
error: String(err)
|
|
8467
|
+
} });
|
|
8468
|
+
}
|
|
7226
8469
|
this.bestFrameTracker.delete(t.trackId);
|
|
8470
|
+
this.objectEmbeddingBestSelector.delete(t.trackId);
|
|
8471
|
+
this.detailDispatcher?.onTrackEnded(t.deviceId, t.trackId);
|
|
7227
8472
|
this.ctx.eventBus.emit({
|
|
7228
8473
|
id: `pa-end-${t.trackId}`,
|
|
7229
8474
|
timestamp: new Date(t.lastSeen),
|
|
@@ -7446,6 +8691,32 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
7446
8691
|
if (input.projection !== "slim" || this.eventMediaBaseUrl === null) return events;
|
|
7447
8692
|
return this.withMediaUrl(events);
|
|
7448
8693
|
}
|
|
8694
|
+
async getKeyEvents(input) {
|
|
8695
|
+
const trackStore = this.trackStore;
|
|
8696
|
+
if (!trackStore) return [];
|
|
8697
|
+
const eventStore = this.eventStore;
|
|
8698
|
+
try {
|
|
8699
|
+
const candidates = await trackStore.queryHistorical({
|
|
8700
|
+
deviceId: input.deviceId,
|
|
8701
|
+
since: input.since,
|
|
8702
|
+
until: input.until,
|
|
8703
|
+
limit: KEY_EVENT_CANDIDATE_CAP
|
|
8704
|
+
});
|
|
8705
|
+
const peakLookup = (trackId) => eventStore ? eventStore.peakForTrack(trackId) : Promise.resolve({
|
|
8706
|
+
peakConfidence: 0,
|
|
8707
|
+
peakBboxAreaFrac: 0,
|
|
8708
|
+
bestEventId: void 0
|
|
8709
|
+
});
|
|
8710
|
+
return await rankKeyEvents(candidates, {
|
|
8711
|
+
limit: input.limit ?? KEY_EVENT_DEFAULT_LIMIT,
|
|
8712
|
+
...input.minImportance !== void 0 ? { minImportance: input.minImportance } : {},
|
|
8713
|
+
...input.classFilter !== void 0 ? { classFilter: input.classFilter } : {}
|
|
8714
|
+
}, peakLookup);
|
|
8715
|
+
} catch (err) {
|
|
8716
|
+
this.ctx.logger.debug("getKeyEvents failed", { meta: { error: String(err) } });
|
|
8717
|
+
return [];
|
|
8718
|
+
}
|
|
8719
|
+
}
|
|
7449
8720
|
async getAudioEvents(input) {
|
|
7450
8721
|
const events = await (this.eventStore?.queryAudio(input) ?? Promise.resolve([]));
|
|
7451
8722
|
if (input.projection !== "slim" || this.eventMediaBaseUrl === null) return events;
|
|
@@ -7488,6 +8759,12 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
7488
8759
|
className: input.classFilter
|
|
7489
8760
|
});
|
|
7490
8761
|
if (embeddingRows.length === 0) return [];
|
|
8762
|
+
const embeddingMediaKeyByTrackId = /* @__PURE__ */ new Map();
|
|
8763
|
+
const keyFrameKeyByTrackId = /* @__PURE__ */ new Map();
|
|
8764
|
+
for (const row of embeddingRows) {
|
|
8765
|
+
if (row.mediaKey !== void 0) embeddingMediaKeyByTrackId.set(row.trackId, row.mediaKey);
|
|
8766
|
+
if (row.keyFrameMediaKey !== void 0) keyFrameKeyByTrackId.set(row.trackId, row.keyFrameMediaKey);
|
|
8767
|
+
}
|
|
7491
8768
|
const encoderInfo = await api.embeddingEncoder.getInfo.query().catch((err) => {
|
|
7492
8769
|
this.ctx.logger.warn("searchObjectEvents: getInfo failed, proceeding without modelId gate", { meta: { error: String(err) } });
|
|
7493
8770
|
return null;
|
|
@@ -7534,10 +8811,20 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
7534
8811
|
score
|
|
7535
8812
|
});
|
|
7536
8813
|
}
|
|
7537
|
-
for (const { event, score } of bestEventByTrackId.values())
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
|
|
8814
|
+
for (const { event, score } of bestEventByTrackId.values()) {
|
|
8815
|
+
const mediaUrl = resolveSearchThumbnailUrl({
|
|
8816
|
+
baseUrl: this.eventMediaBaseUrl,
|
|
8817
|
+
eventId: event.id,
|
|
8818
|
+
...event.trackId !== void 0 && embeddingMediaKeyByTrackId.has(event.trackId) ? { embeddingMediaKey: embeddingMediaKeyByTrackId.get(event.trackId) } : {}
|
|
8819
|
+
});
|
|
8820
|
+
const keyFrameMediaKey = event.trackId !== void 0 ? keyFrameKeyByTrackId.get(event.trackId) : void 0;
|
|
8821
|
+
scored.push({
|
|
8822
|
+
...event,
|
|
8823
|
+
score,
|
|
8824
|
+
...mediaUrl !== void 0 ? { mediaUrl } : {},
|
|
8825
|
+
...keyFrameMediaKey !== void 0 ? { keyFrameMediaKey } : {}
|
|
8826
|
+
});
|
|
8827
|
+
}
|
|
7541
8828
|
}
|
|
7542
8829
|
scored.sort((a, b) => b.score - a.score);
|
|
7543
8830
|
return scored.slice(0, input.limit);
|
|
@@ -7569,6 +8856,24 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
7569
8856
|
* (native-res boxed frame) → any available file. Returns `null` if the
|
|
7570
8857
|
* event has no media at all.
|
|
7571
8858
|
*/
|
|
8859
|
+
/**
|
|
8860
|
+
* Data-plane resolver: an id is EITHER a bare event id (a UUID → the event's
|
|
8861
|
+
* crop, today's behaviour) OR a MediaStore key (`ownerKind:ownerId:kind:ts`,
|
|
8862
|
+
* contains ':' → served directly by key). The object-embedding search hit
|
|
8863
|
+
* points its thumbnail at the embedding row's crop key (a `track:…:crop:…`
|
|
8864
|
+
* key), so this resolves that crop; event ids stay on the event-crop path.
|
|
8865
|
+
*/
|
|
8866
|
+
async readMediaByEventOrKey(id) {
|
|
8867
|
+
if (id.includes(":")) {
|
|
8868
|
+
const file = await (this.mediaStore?.getByKey(id) ?? Promise.resolve(null));
|
|
8869
|
+
if (!file) return null;
|
|
8870
|
+
return {
|
|
8871
|
+
bytes: Buffer.from(file.base64, "base64"),
|
|
8872
|
+
key: file.key
|
|
8873
|
+
};
|
|
8874
|
+
}
|
|
8875
|
+
return this.readEventThumbnail(id);
|
|
8876
|
+
}
|
|
7572
8877
|
async readEventThumbnail(eventId) {
|
|
7573
8878
|
const files = await (this.mediaStore?.listByOwner("event", eventId) ?? Promise.resolve([]));
|
|
7574
8879
|
const chosen = files.find((f) => f.kind === "crop") ?? files.find((f) => f.kind === "fullFrameBoxed") ?? files[0];
|