@camstack/addon-post-analysis 1.1.24 → 1.1.26
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-B0VgBdBN.js → dist-BEx5ST1W.js} +144 -6
- package/dist/{dist-DRK78NMq.mjs → dist-DytVmDZg.mjs} +139 -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-Cx0LM0Sq.js → node-Cg_cGqs0.js} +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BsRL9Bh-.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-p-r6oXq8.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-C6LbOCa9.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-sO1Drmtf.mjs → hostInit-2UZIpU0W.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +1091 -110
- package/dist/pipeline-analytics/index.mjs +1089 -108
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/dist/{resolve-frame-DBXwF5fk.js → resolve-frame-Cbm_NFuq.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-BEx5ST1W.js");
|
|
6
|
+
const require_resolve_frame = require("../resolve-frame-Cbm_NFuq.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
|
}
|
|
@@ -1429,6 +1569,10 @@ var FrameProcessor = class {
|
|
|
1429
1569
|
w: det.bbox.width,
|
|
1430
1570
|
h: det.bbox.height
|
|
1431
1571
|
});
|
|
1572
|
+
if (det.embedding !== void 0 && !embeddingByBbox.has(parentBbox)) embeddingByBbox.set(parentBbox, {
|
|
1573
|
+
embedding: det.embedding,
|
|
1574
|
+
...det.embeddingModelId !== void 0 ? { embeddingModelId: det.embeddingModelId } : {}
|
|
1575
|
+
});
|
|
1432
1576
|
}
|
|
1433
1577
|
for (const det of frame.detections) {
|
|
1434
1578
|
if (det.kind !== "detail" || det.macroClass !== "plate" || !det.parentId) continue;
|
|
@@ -1532,6 +1676,152 @@ var FrameProcessor = class {
|
|
|
1532
1676
|
}
|
|
1533
1677
|
};
|
|
1534
1678
|
//#endregion
|
|
1679
|
+
//#region src/pipeline-analytics/pipeline/best-detection-tracker.ts
|
|
1680
|
+
var BestDetectionTracker = class {
|
|
1681
|
+
hysteresis;
|
|
1682
|
+
minGapMs;
|
|
1683
|
+
best = /* @__PURE__ */ new Map();
|
|
1684
|
+
constructor(options = {}) {
|
|
1685
|
+
this.hysteresis = options.hysteresis ?? 0;
|
|
1686
|
+
this.minGapMs = options.minGapMs ?? 0;
|
|
1687
|
+
}
|
|
1688
|
+
/**
|
|
1689
|
+
* Record a detection's `confidence` (at wall-clock `timestamp`) for `trackId`.
|
|
1690
|
+
* Returns true when it becomes the track's new best — the first sighting, or a
|
|
1691
|
+
* confidence that beats the held peak by more than `hysteresis` AND respects
|
|
1692
|
+
* `minGapMs`. On acceptance the held peak is advanced to this observation.
|
|
1693
|
+
*/
|
|
1694
|
+
observe(trackId, confidence, timestamp) {
|
|
1695
|
+
const cur = this.best.get(trackId);
|
|
1696
|
+
const isNewBest = cur === void 0 || confidence > cur.confidence + this.hysteresis && timestamp - cur.atMs >= this.minGapMs;
|
|
1697
|
+
if (isNewBest) this.best.set(trackId, {
|
|
1698
|
+
confidence,
|
|
1699
|
+
atMs: timestamp
|
|
1700
|
+
});
|
|
1701
|
+
return isNewBest;
|
|
1702
|
+
}
|
|
1703
|
+
/** The held peak for a track (undefined if never observed). */
|
|
1704
|
+
peak(trackId) {
|
|
1705
|
+
return this.best.get(trackId);
|
|
1706
|
+
}
|
|
1707
|
+
/** Drop a track's peak (call at track end). */
|
|
1708
|
+
delete(trackId) {
|
|
1709
|
+
this.best.delete(trackId);
|
|
1710
|
+
}
|
|
1711
|
+
clear() {
|
|
1712
|
+
this.best.clear();
|
|
1713
|
+
}
|
|
1714
|
+
};
|
|
1715
|
+
//#endregion
|
|
1716
|
+
//#region src/pipeline-analytics/pipeline/track-best-detection.ts
|
|
1717
|
+
/**
|
|
1718
|
+
* `TrackBestSelector` — the ONE unified "best detection per track" primitive.
|
|
1719
|
+
*
|
|
1720
|
+
* Post-analysis derives several per-track "best" artefacts (best FRAME thumbnail,
|
|
1721
|
+
* best OBJECT/CLIP embedding + its crop, best FACE crop). They all rank the same
|
|
1722
|
+
* way: highest detector confidence per `trackId`. This selector composes the
|
|
1723
|
+
* canonical {@link BestDetectionTracker} ranking with the RESOLVED payload the
|
|
1724
|
+
* consumers need at the peak (bbox / className / embedding / faceBbox), so a
|
|
1725
|
+
* single "new best" DECISION can drive one capture whose output is shared:
|
|
1726
|
+
* - the boxed best-frame `thumbnail`,
|
|
1727
|
+
* - the tight object crop written onto the CLIP embedding row's `mediaKey`
|
|
1728
|
+
* (so a search hit's thumbnail IS the embedded crop).
|
|
1729
|
+
*
|
|
1730
|
+
* It is deliberately in-memory (the peak is per live track, dropped at track
|
|
1731
|
+
* end). The CLIP embedding's cross-restart persistence stays a SEPARATE store-
|
|
1732
|
+
* side gate (`ObjectEmbeddingStore.upsertIfBetter`): this selector unifies the
|
|
1733
|
+
* DECISION, not the durable store (see best-detection-tracker.ts docstring).
|
|
1734
|
+
*
|
|
1735
|
+
* The face path keeps its own hold buffer because a face crop can only come
|
|
1736
|
+
* from a face-bearing frame (the documented FRAME↔FACE seam) — but it shares
|
|
1737
|
+
* this ranking so best-frame and best-face agree on which frame is "best".
|
|
1738
|
+
*/
|
|
1739
|
+
var TrackBestSelector = class {
|
|
1740
|
+
tracker;
|
|
1741
|
+
payloads = /* @__PURE__ */ new Map();
|
|
1742
|
+
constructor(options = {}) {
|
|
1743
|
+
this.tracker = new BestDetectionTracker(options);
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* Record an observation for its track. Returns true when it becomes the
|
|
1747
|
+
* track's new best (first sighting, or a confidence that beats the held peak
|
|
1748
|
+
* per the tracker's hysteresis / minGap rules). On acceptance the held payload
|
|
1749
|
+
* advances to this observation so `peak(trackId)` returns the winning frame's
|
|
1750
|
+
* bbox / embedding / faceBbox.
|
|
1751
|
+
*/
|
|
1752
|
+
observe(obs) {
|
|
1753
|
+
const isNewBest = this.tracker.observe(obs.trackId, obs.confidence, obs.atMs);
|
|
1754
|
+
if (isNewBest) {
|
|
1755
|
+
const { trackId, ...payload } = obs;
|
|
1756
|
+
this.payloads.set(trackId, payload);
|
|
1757
|
+
}
|
|
1758
|
+
return isNewBest;
|
|
1759
|
+
}
|
|
1760
|
+
/** The held best payload for a track (undefined if never observed). */
|
|
1761
|
+
peak(trackId) {
|
|
1762
|
+
return this.payloads.get(trackId);
|
|
1763
|
+
}
|
|
1764
|
+
/** The held peak confidence for a track (undefined if never observed). */
|
|
1765
|
+
peakConfidence(trackId) {
|
|
1766
|
+
return this.tracker.peak(trackId)?.confidence;
|
|
1767
|
+
}
|
|
1768
|
+
/** Drop a track's peak + payload (call at track end). */
|
|
1769
|
+
delete(trackId) {
|
|
1770
|
+
this.tracker.delete(trackId);
|
|
1771
|
+
this.payloads.delete(trackId);
|
|
1772
|
+
}
|
|
1773
|
+
clear() {
|
|
1774
|
+
this.tracker.clear();
|
|
1775
|
+
this.payloads.clear();
|
|
1776
|
+
}
|
|
1777
|
+
};
|
|
1778
|
+
//#endregion
|
|
1779
|
+
//#region src/pipeline-analytics/pipeline/object-embedding-selection.ts
|
|
1780
|
+
function isClipObjectEmbedding(t) {
|
|
1781
|
+
return Array.isArray(t.embedding) && t.embedding.length > 0 && typeof t.embeddingModelId === "string" && t.embeddingModelId.startsWith("mobileclip-");
|
|
1782
|
+
}
|
|
1783
|
+
function resolveSearchThumbnailUrl(input) {
|
|
1784
|
+
if (input.baseUrl === null) return void 0;
|
|
1785
|
+
const id = input.embeddingMediaKey ?? input.eventId;
|
|
1786
|
+
return `${input.baseUrl}/${encodeURIComponent(id)}`;
|
|
1787
|
+
}
|
|
1788
|
+
//#endregion
|
|
1789
|
+
//#region src/pipeline-analytics/pipeline/key-event-query.ts
|
|
1790
|
+
async function rankKeyEvents(candidates, options, peakLookup) {
|
|
1791
|
+
const scored = [];
|
|
1792
|
+
for (const t of candidates) {
|
|
1793
|
+
if (options.classFilter !== void 0 && t.className !== options.classFilter) continue;
|
|
1794
|
+
let importance = t.importance;
|
|
1795
|
+
let bestEventId = t.bestEventId;
|
|
1796
|
+
if (importance === void 0) {
|
|
1797
|
+
const peak = await peakLookup(t.trackId);
|
|
1798
|
+
importance = computeImportance({
|
|
1799
|
+
peakConfidence: peak.peakConfidence,
|
|
1800
|
+
className: t.className,
|
|
1801
|
+
durationMs: t.lastSeen - t.firstSeen,
|
|
1802
|
+
peakBboxAreaFrac: peak.peakBboxAreaFrac,
|
|
1803
|
+
totalDistance: t.totalDistance,
|
|
1804
|
+
zonesVisited: t.zonesVisited,
|
|
1805
|
+
...t.label !== void 0 ? { label: t.label } : {}
|
|
1806
|
+
}).importance;
|
|
1807
|
+
bestEventId = bestEventId ?? peak.bestEventId;
|
|
1808
|
+
}
|
|
1809
|
+
if (options.minImportance !== void 0 && importance < options.minImportance) continue;
|
|
1810
|
+
scored.push({
|
|
1811
|
+
id: bestEventId ?? t.trackId,
|
|
1812
|
+
trackId: t.trackId,
|
|
1813
|
+
timestamp: t.firstSeen,
|
|
1814
|
+
className: t.className,
|
|
1815
|
+
...t.label !== void 0 ? { label: t.label } : {},
|
|
1816
|
+
importance,
|
|
1817
|
+
bestEventId: bestEventId ?? "",
|
|
1818
|
+
windowMs: t.lastSeen - t.firstSeen
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
scored.sort((a, b) => b.importance - a.importance);
|
|
1822
|
+
return scored.slice(0, options.limit);
|
|
1823
|
+
}
|
|
1824
|
+
//#endregion
|
|
1535
1825
|
//#region src/pipeline-analytics/pipeline/native-detection.ts
|
|
1536
1826
|
/**
|
|
1537
1827
|
* Nominal frame size used to denormalize native `[0,1]` boxes when a
|
|
@@ -1701,6 +1991,18 @@ var TRACKS_COLUMNS = [
|
|
|
1701
1991
|
{
|
|
1702
1992
|
name: "state",
|
|
1703
1993
|
type: "TEXT"
|
|
1994
|
+
},
|
|
1995
|
+
{
|
|
1996
|
+
name: "importance",
|
|
1997
|
+
type: "REAL"
|
|
1998
|
+
},
|
|
1999
|
+
{
|
|
2000
|
+
name: "bestEventId",
|
|
2001
|
+
type: "TEXT"
|
|
2002
|
+
},
|
|
2003
|
+
{
|
|
2004
|
+
name: "importanceReason",
|
|
2005
|
+
type: "TEXT"
|
|
1704
2006
|
}
|
|
1705
2007
|
];
|
|
1706
2008
|
var TRACKS_INDEXES = [{
|
|
@@ -1732,7 +2034,10 @@ function cloneTrack(t) {
|
|
|
1732
2034
|
zonesVisited: [...t.zonesVisited],
|
|
1733
2035
|
totalDistance: t.totalDistance,
|
|
1734
2036
|
state: t.state,
|
|
1735
|
-
active: t.active
|
|
2037
|
+
active: t.active,
|
|
2038
|
+
...t.importance !== void 0 ? { importance: t.importance } : {},
|
|
2039
|
+
...t.bestEventId !== void 0 ? { bestEventId: t.bestEventId } : {},
|
|
2040
|
+
...t.importanceReason !== void 0 ? { importanceReason: t.importanceReason } : {}
|
|
1736
2041
|
};
|
|
1737
2042
|
}
|
|
1738
2043
|
var TrackStore = class {
|
|
@@ -1801,6 +2106,16 @@ var TrackStore = class {
|
|
|
1801
2106
|
lastSnapshotAt(trackId) {
|
|
1802
2107
|
return this.active.get(trackId)?.lastSnapshotAt ?? 0;
|
|
1803
2108
|
}
|
|
2109
|
+
/**
|
|
2110
|
+
* Seed the snapshot cadence clock (once, at track start) WITHOUT appending a
|
|
2111
|
+
* snapshot, so the first periodic snapshot fires ~snapshotIntervalMs after the
|
|
2112
|
+
* track begins rather than immediately — the `firstFrame` already covers the
|
|
2113
|
+
* track's start. No-op if a snapshot was already taken (clock already set).
|
|
2114
|
+
*/
|
|
2115
|
+
seedSnapshotClock(trackId, timestamp) {
|
|
2116
|
+
const t = this.active.get(trackId);
|
|
2117
|
+
if (t && t.lastSnapshotAt === 0) t.lastSnapshotAt = timestamp;
|
|
2118
|
+
}
|
|
1804
2119
|
getActive(deviceId) {
|
|
1805
2120
|
const out = [];
|
|
1806
2121
|
for (const t of this.active.values()) if (t.deviceId === deviceId && t.active) out.push(cloneTrack(t));
|
|
@@ -1856,6 +2171,37 @@ var TrackStore = class {
|
|
|
1856
2171
|
}
|
|
1857
2172
|
}
|
|
1858
2173
|
/**
|
|
2174
|
+
* Stamp a track's importance score (+ dominant reason and best-event pointer).
|
|
2175
|
+
* Updates the in-memory active entry (so the value is carried into an eventual
|
|
2176
|
+
* (re)persist) AND patches the already-persisted row. Mirrors `setLabel`.
|
|
2177
|
+
* Forward-only — never rewrites history beyond these fields.
|
|
2178
|
+
*/
|
|
2179
|
+
async setImportance(trackId, importance, reason, bestEventId) {
|
|
2180
|
+
const active = this.active.get(trackId);
|
|
2181
|
+
if (active) {
|
|
2182
|
+
active.importance = importance;
|
|
2183
|
+
active.importanceReason = reason;
|
|
2184
|
+
if (bestEventId !== void 0) active.bestEventId = bestEventId;
|
|
2185
|
+
}
|
|
2186
|
+
const data = {
|
|
2187
|
+
importance,
|
|
2188
|
+
importanceReason: reason
|
|
2189
|
+
};
|
|
2190
|
+
if (bestEventId !== void 0) data["bestEventId"] = bestEventId;
|
|
2191
|
+
try {
|
|
2192
|
+
await this.store.update.mutate({
|
|
2193
|
+
collection: TRACKS_COLLECTION,
|
|
2194
|
+
id: trackId,
|
|
2195
|
+
data
|
|
2196
|
+
});
|
|
2197
|
+
} catch (err) {
|
|
2198
|
+
this.logger.warn("setImportance persist failed", { meta: {
|
|
2199
|
+
trackId,
|
|
2200
|
+
error: String(err)
|
|
2201
|
+
} });
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
/**
|
|
1859
2205
|
* Clear a track's label. Sets label to null in the persisted row (so
|
|
1860
2206
|
* rowToTrack's `typeof label === 'string'` guard omits it on read → label
|
|
1861
2207
|
* is absent/undefined). Also clears the in-memory active entry if present.
|
|
@@ -1926,7 +2272,10 @@ var TrackStore = class {
|
|
|
1926
2272
|
snapshots: [...t.snapshots],
|
|
1927
2273
|
zonesVisited: [...t.zonesVisited],
|
|
1928
2274
|
totalDistance: t.totalDistance,
|
|
1929
|
-
state: t.state
|
|
2275
|
+
state: t.state,
|
|
2276
|
+
...t.importance !== void 0 ? { importance: t.importance } : {},
|
|
2277
|
+
...t.bestEventId !== void 0 ? { bestEventId: t.bestEventId } : {},
|
|
2278
|
+
...t.importanceReason !== void 0 ? { importanceReason: t.importanceReason } : {}
|
|
1930
2279
|
}
|
|
1931
2280
|
});
|
|
1932
2281
|
}
|
|
@@ -1935,6 +2284,9 @@ var TrackStore = class {
|
|
|
1935
2284
|
const snapshots = data["snapshots"] ?? [];
|
|
1936
2285
|
const zones = data["zonesVisited"] ?? [];
|
|
1937
2286
|
const label = data["label"];
|
|
2287
|
+
const importance = data["importance"];
|
|
2288
|
+
const bestEventId = data["bestEventId"];
|
|
2289
|
+
const importanceReason = data["importanceReason"];
|
|
1938
2290
|
return {
|
|
1939
2291
|
trackId: id,
|
|
1940
2292
|
deviceId: Number(data["deviceId"]),
|
|
@@ -1947,7 +2299,10 @@ var TrackStore = class {
|
|
|
1947
2299
|
zonesVisited: zones,
|
|
1948
2300
|
totalDistance: Number(data["totalDistance"] ?? 0),
|
|
1949
2301
|
state: data["state"] ?? "idle",
|
|
1950
|
-
active: false
|
|
2302
|
+
active: false,
|
|
2303
|
+
...typeof importance === "number" ? { importance } : {},
|
|
2304
|
+
...typeof bestEventId === "string" ? { bestEventId } : {},
|
|
2305
|
+
...typeof importanceReason === "string" ? { importanceReason } : {}
|
|
1951
2306
|
};
|
|
1952
2307
|
}
|
|
1953
2308
|
};
|
|
@@ -2065,6 +2420,46 @@ var MediaStore = class {
|
|
|
2065
2420
|
}
|
|
2066
2421
|
}
|
|
2067
2422
|
/**
|
|
2423
|
+
* Overwrite semantics: keep exactly ONE media entry per (ownerKind, ownerId,
|
|
2424
|
+
* kind). Used for the rolling `lastFrame` and the best `thumbnail` per track —
|
|
2425
|
+
* each new capture replaces the previous one (blob + index row) rather than
|
|
2426
|
+
* accumulating a filmstrip the way `put` does. Deletes any existing rows of
|
|
2427
|
+
* that (owner, kind) first, then writes the fresh one. Returns the new key.
|
|
2428
|
+
*/
|
|
2429
|
+
async putReplacing(params) {
|
|
2430
|
+
const existing = await this.store.query.query({
|
|
2431
|
+
collection: MEDIA_COLLECTION,
|
|
2432
|
+
filter: { where: {
|
|
2433
|
+
ownerKind: params.ownerKind,
|
|
2434
|
+
ownerId: params.ownerId,
|
|
2435
|
+
kind: params.kind
|
|
2436
|
+
} }
|
|
2437
|
+
});
|
|
2438
|
+
const newKey = await this.put(params);
|
|
2439
|
+
for (const row of existing) {
|
|
2440
|
+
if (row.id === newKey) continue;
|
|
2441
|
+
const path = String(row.data["path"] ?? "");
|
|
2442
|
+
if (path) try {
|
|
2443
|
+
await this.storage.delete({
|
|
2444
|
+
location: "eventMedia",
|
|
2445
|
+
relativePath: path
|
|
2446
|
+
});
|
|
2447
|
+
} catch {}
|
|
2448
|
+
try {
|
|
2449
|
+
await this.store.delete.mutate({
|
|
2450
|
+
collection: MEDIA_COLLECTION,
|
|
2451
|
+
key: row.id
|
|
2452
|
+
});
|
|
2453
|
+
} catch (err) {
|
|
2454
|
+
this.logger.debug("media putReplacing: stale row delete failed", { meta: {
|
|
2455
|
+
key: row.id,
|
|
2456
|
+
error: String(err)
|
|
2457
|
+
} });
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
return newKey;
|
|
2461
|
+
}
|
|
2462
|
+
/**
|
|
2068
2463
|
* Fetch one media entry by its key (id). Returns null if the key is not
|
|
2069
2464
|
* found in the index or if the blob is missing from storage.
|
|
2070
2465
|
*/
|
|
@@ -2410,6 +2805,10 @@ var OBJECT_COLUMNS = [
|
|
|
2410
2805
|
{
|
|
2411
2806
|
name: "mediaKey",
|
|
2412
2807
|
type: "TEXT"
|
|
2808
|
+
},
|
|
2809
|
+
{
|
|
2810
|
+
name: "importance",
|
|
2811
|
+
type: "REAL"
|
|
2413
2812
|
}
|
|
2414
2813
|
];
|
|
2415
2814
|
var AUDIO_COLUMNS = [
|
|
@@ -2607,6 +3006,63 @@ var EventStore = class {
|
|
|
2607
3006
|
return updated;
|
|
2608
3007
|
}
|
|
2609
3008
|
/**
|
|
3009
|
+
* Forward-only: stamp `importance` on every already-emitted object event of a
|
|
3010
|
+
* track. Returns the number of events updated. Best-effort per row. Mirrors
|
|
3011
|
+
* `setLabelForTrack`. Called when a track's key-event score is computed (at
|
|
3012
|
+
* expiry) or recomputed (late label) so an event row carries the parent
|
|
3013
|
+
* track's importance without a join.
|
|
3014
|
+
*/
|
|
3015
|
+
async setImportanceForTrack(trackId, importance) {
|
|
3016
|
+
const rows = await this.store.query.query({
|
|
3017
|
+
collection: OBJECT_EVENTS_COLLECTION,
|
|
3018
|
+
filter: { where: { trackId } }
|
|
3019
|
+
});
|
|
3020
|
+
let updated = 0;
|
|
3021
|
+
for (const row of rows) try {
|
|
3022
|
+
await this.store.update.mutate({
|
|
3023
|
+
collection: OBJECT_EVENTS_COLLECTION,
|
|
3024
|
+
id: row.id,
|
|
3025
|
+
data: { importance }
|
|
3026
|
+
});
|
|
3027
|
+
updated++;
|
|
3028
|
+
} catch (err) {
|
|
3029
|
+
this.logger.warn("setImportanceForTrack update failed", { meta: {
|
|
3030
|
+
trackId,
|
|
3031
|
+
eventId: row.id,
|
|
3032
|
+
error: String(err)
|
|
3033
|
+
} });
|
|
3034
|
+
}
|
|
3035
|
+
return updated;
|
|
3036
|
+
}
|
|
3037
|
+
/**
|
|
3038
|
+
* The track's highest-confidence object event, its bbox area (as a fraction of
|
|
3039
|
+
* frame area), and that event's id — the SHARED per-track ranking already used
|
|
3040
|
+
* for the best frame, read back from the persisted object events (index
|
|
3041
|
+
* `idx_object_track`). Returns zeros + undefined id when the track has none.
|
|
3042
|
+
* Used by the importance scorer at expiry and by `getKeyEvents` compute-on-read.
|
|
3043
|
+
*/
|
|
3044
|
+
async peakForTrack(trackId) {
|
|
3045
|
+
const rows = await this.store.query.query({
|
|
3046
|
+
collection: OBJECT_EVENTS_COLLECTION,
|
|
3047
|
+
filter: { where: { trackId } }
|
|
3048
|
+
});
|
|
3049
|
+
let bestConf = -1;
|
|
3050
|
+
let bestEventId;
|
|
3051
|
+
let peakBboxAreaFrac = 0;
|
|
3052
|
+
for (const row of rows) {
|
|
3053
|
+
const conf = typeof row.data["confidence"] === "number" ? row.data["confidence"] : 0;
|
|
3054
|
+
if (conf <= bestConf) continue;
|
|
3055
|
+
bestConf = conf;
|
|
3056
|
+
bestEventId = row.id;
|
|
3057
|
+
peakBboxAreaFrac = bboxAreaFrac(row.data);
|
|
3058
|
+
}
|
|
3059
|
+
return {
|
|
3060
|
+
peakConfidence: bestConf < 0 ? 0 : bestConf,
|
|
3061
|
+
peakBboxAreaFrac,
|
|
3062
|
+
bestEventId
|
|
3063
|
+
};
|
|
3064
|
+
}
|
|
3065
|
+
/**
|
|
2610
3066
|
* Clear `label` on every already-emitted object event of a track (sets label
|
|
2611
3067
|
* to null so stripNulls/slimObject omit it on read → label is absent/undefined).
|
|
2612
3068
|
* Returns the number of events updated. Best-effort per row. Mirrors
|
|
@@ -2801,7 +3257,8 @@ function slimObject(id, data) {
|
|
|
2801
3257
|
timestamp: data["timestamp"],
|
|
2802
3258
|
className: data["className"],
|
|
2803
3259
|
...typeof data["frameId"] === "string" ? { frameId: data["frameId"] } : {},
|
|
2804
|
-
...typeof data["trackId"] === "string" ? { trackId: data["trackId"] } : {}
|
|
3260
|
+
...typeof data["trackId"] === "string" ? { trackId: data["trackId"] } : {},
|
|
3261
|
+
...typeof data["importance"] === "number" ? { importance: data["importance"] } : {}
|
|
2805
3262
|
};
|
|
2806
3263
|
if (typeof data["label"] === "string") return {
|
|
2807
3264
|
...base,
|
|
@@ -2831,6 +3288,17 @@ function slimAudio(id, data) {
|
|
|
2831
3288
|
}
|
|
2832
3289
|
return base;
|
|
2833
3290
|
}
|
|
3291
|
+
function bboxAreaFrac(data) {
|
|
3292
|
+
const bbox = data["bbox"];
|
|
3293
|
+
const fw = data["frameWidth"];
|
|
3294
|
+
const fh = data["frameHeight"];
|
|
3295
|
+
if (bbox === null || typeof bbox !== "object") return 0;
|
|
3296
|
+
if (typeof fw !== "number" || typeof fh !== "number" || fw <= 0 || fh <= 0) return 0;
|
|
3297
|
+
const w = "w" in bbox && typeof bbox.w === "number" ? bbox.w : 0;
|
|
3298
|
+
const h = "h" in bbox && typeof bbox.h === "number" ? bbox.h : 0;
|
|
3299
|
+
if (w <= 0 || h <= 0) return 0;
|
|
3300
|
+
return w * h / (fw * fh);
|
|
3301
|
+
}
|
|
2834
3302
|
function stripNulls(data) {
|
|
2835
3303
|
const out = {};
|
|
2836
3304
|
for (const [k, v] of Object.entries(data)) if (v !== null) out[k] = v;
|
|
@@ -2977,7 +3445,9 @@ var EventMediaDispatcher = class {
|
|
|
2977
3445
|
}
|
|
2978
3446
|
async captureForFrame(input) {
|
|
2979
3447
|
const { deviceId, frameHandle, events, trackFrames } = input;
|
|
2980
|
-
|
|
3448
|
+
const snapshots = input.snapshots ?? [];
|
|
3449
|
+
const empty = { storedSnapshots: [] };
|
|
3450
|
+
if (events.length === 0 && trackFrames.length === 0 && snapshots.length === 0) return empty;
|
|
2981
3451
|
let decoded;
|
|
2982
3452
|
try {
|
|
2983
3453
|
decoded = await require_resolve_frame.resolveFrame(frameHandle, {
|
|
@@ -2994,7 +3464,7 @@ var EventMediaDispatcher = class {
|
|
|
2994
3464
|
error: String(err)
|
|
2995
3465
|
}
|
|
2996
3466
|
});
|
|
2997
|
-
return;
|
|
3467
|
+
return empty;
|
|
2998
3468
|
}
|
|
2999
3469
|
if (!decoded) {
|
|
3000
3470
|
this.deps.logger.debug("event media: frame recycled before resolve", {
|
|
@@ -3004,7 +3474,7 @@ var EventMediaDispatcher = class {
|
|
|
3004
3474
|
shmId: frameHandle.shmId
|
|
3005
3475
|
}
|
|
3006
3476
|
});
|
|
3007
|
-
return;
|
|
3477
|
+
return empty;
|
|
3008
3478
|
}
|
|
3009
3479
|
if (decoded.format !== "rgb") {
|
|
3010
3480
|
this.deps.logger.debug("event media: resolved frame is not RGB", {
|
|
@@ -3014,13 +3484,87 @@ var EventMediaDispatcher = class {
|
|
|
3014
3484
|
format: decoded.format
|
|
3015
3485
|
}
|
|
3016
3486
|
});
|
|
3017
|
-
return;
|
|
3487
|
+
return empty;
|
|
3018
3488
|
}
|
|
3019
3489
|
const frameData = Buffer.isBuffer(decoded.data) ? decoded.data : Buffer.from(decoded.data);
|
|
3020
3490
|
const fw = decoded.width;
|
|
3021
3491
|
const fh = decoded.height;
|
|
3022
3492
|
for (const ev of events) await this.writeEventMedia(deviceId, frameData, fw, fh, ev, input.cropPadding);
|
|
3023
3493
|
for (const tf of trackFrames) await this.writeTrackFrame(deviceId, frameData, fw, fh, tf);
|
|
3494
|
+
const storedSnapshots = [];
|
|
3495
|
+
for (const sn of snapshots) {
|
|
3496
|
+
const stored = await this.writeTrackSnapshot(deviceId, frameData, fw, fh, sn);
|
|
3497
|
+
if (stored) storedSnapshots.push(stored);
|
|
3498
|
+
}
|
|
3499
|
+
return { storedSnapshots };
|
|
3500
|
+
}
|
|
3501
|
+
/**
|
|
3502
|
+
* Periodic per-track media (§5). Encodes the boxed frame ONCE and fans it out
|
|
3503
|
+
* to whichever of the three destinations is requested: an appended `snapshot`
|
|
3504
|
+
* (timeline filmstrip), the rolling `lastFrame` (overwrite), and the best
|
|
3505
|
+
* `thumbnail` (overwrite). Returns the appended snapshot for TrackStore wiring
|
|
3506
|
+
* (null when `appendSnapshot` is false or the encode failed).
|
|
3507
|
+
*/
|
|
3508
|
+
async writeTrackSnapshot(deviceId, frameData, fw, fh, sn) {
|
|
3509
|
+
if (!sn.appendSnapshot && !sn.rollingLastFrame && !sn.bestThumbnail) return null;
|
|
3510
|
+
let boxed;
|
|
3511
|
+
try {
|
|
3512
|
+
boxed = await drawBoxedFrame(frameData, fw, fh, [{
|
|
3513
|
+
...sn.bbox,
|
|
3514
|
+
...sn.label ? { label: sn.label } : {}
|
|
3515
|
+
}], { quality: MEDIA_QUALITY });
|
|
3516
|
+
} catch (err) {
|
|
3517
|
+
this.deps.logger.warn("event media: track snapshot encode failed", {
|
|
3518
|
+
tags: { deviceId },
|
|
3519
|
+
meta: {
|
|
3520
|
+
deviceId,
|
|
3521
|
+
trackId: sn.trackId,
|
|
3522
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3523
|
+
}
|
|
3524
|
+
});
|
|
3525
|
+
return null;
|
|
3526
|
+
}
|
|
3527
|
+
let stored = null;
|
|
3528
|
+
if (sn.appendSnapshot) try {
|
|
3529
|
+
const mediaKey = await this.deps.mediaStore.put({
|
|
3530
|
+
deviceId,
|
|
3531
|
+
ownerKind: "track",
|
|
3532
|
+
ownerId: sn.trackId,
|
|
3533
|
+
kind: "snapshot",
|
|
3534
|
+
timestamp: sn.timestamp,
|
|
3535
|
+
data: boxed
|
|
3536
|
+
});
|
|
3537
|
+
stored = {
|
|
3538
|
+
trackId: sn.trackId,
|
|
3539
|
+
mediaKey,
|
|
3540
|
+
timestamp: sn.timestamp,
|
|
3541
|
+
bbox: sn.bbox
|
|
3542
|
+
};
|
|
3543
|
+
} catch {}
|
|
3544
|
+
if (sn.rollingLastFrame) await this.replaceKind(deviceId, sn.trackId, "lastFrame", sn.timestamp, boxed);
|
|
3545
|
+
if (sn.bestThumbnail) await this.replaceKind(deviceId, sn.trackId, "thumbnail", sn.timestamp, boxed);
|
|
3546
|
+
return stored;
|
|
3547
|
+
}
|
|
3548
|
+
async replaceKind(deviceId, trackId, kind, timestamp, data) {
|
|
3549
|
+
try {
|
|
3550
|
+
await this.deps.mediaStore.putReplacing({
|
|
3551
|
+
deviceId,
|
|
3552
|
+
ownerKind: "track",
|
|
3553
|
+
ownerId: trackId,
|
|
3554
|
+
kind,
|
|
3555
|
+
timestamp,
|
|
3556
|
+
data
|
|
3557
|
+
});
|
|
3558
|
+
} catch (err) {
|
|
3559
|
+
this.deps.logger.debug(`event media: ${kind} replace failed`, {
|
|
3560
|
+
tags: { deviceId },
|
|
3561
|
+
meta: {
|
|
3562
|
+
deviceId,
|
|
3563
|
+
trackId,
|
|
3564
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3565
|
+
}
|
|
3566
|
+
});
|
|
3567
|
+
}
|
|
3024
3568
|
}
|
|
3025
3569
|
async writeEventMedia(deviceId, frameData, fw, fh, ev, cropPadding) {
|
|
3026
3570
|
const box = {
|
|
@@ -4015,11 +4559,20 @@ function resolveFaceSettings(raw) {
|
|
|
4015
4559
|
* its default — parse never throws). Mirrors `face-settings` /
|
|
4016
4560
|
* `audio-detection-settings`.
|
|
4017
4561
|
*/
|
|
4018
|
-
var MediaSettingsSchema = require_dist.object({
|
|
4019
|
-
/** Fractional padding added around a detection bbox before cropping.
|
|
4020
|
-
* 0 = tight crop, 0.15 = expand each side by 15% of the box dimension,
|
|
4021
|
-
* max 2 (200%). */
|
|
4022
|
-
cropPadding: require_dist.number().min(0).max(2).default(.15)
|
|
4562
|
+
var MediaSettingsSchema = require_dist.object({
|
|
4563
|
+
/** Fractional padding added around a detection bbox before cropping.
|
|
4564
|
+
* 0 = tight crop, 0.15 = expand each side by 15% of the box dimension,
|
|
4565
|
+
* max 2 (200%). */
|
|
4566
|
+
cropPadding: require_dist.number().min(0).max(2).default(.15),
|
|
4567
|
+
/** Master switch for periodic per-track snapshots (the timeline filmstrip +
|
|
4568
|
+
* the rolling `lastFrame` + the best `thumbnail`). When false, only the
|
|
4569
|
+
* per-track `firstFrame` and per-event media are produced. */
|
|
4570
|
+
saveThumbnails: require_dist.boolean().default(true),
|
|
4571
|
+
/** Cadence (ms) for the periodic per-track `snapshot` + rolling `lastFrame`.
|
|
4572
|
+
* A snapshot is captured for an active track only after this much wall-clock
|
|
4573
|
+
* has elapsed since its previous one. */
|
|
4574
|
+
snapshotIntervalMs: require_dist.number().int().min(500).max(6e4).default(5e3)
|
|
4575
|
+
});
|
|
4023
4576
|
var MEDIA_DEFAULTS = MediaSettingsSchema.parse({});
|
|
4024
4577
|
/**
|
|
4025
4578
|
* Resolve a per-device store blob into typed media settings. Unknown/invalid
|
|
@@ -4030,7 +4583,11 @@ function resolveMediaSettings(raw) {
|
|
|
4030
4583
|
const parsed = MediaSettingsSchema.shape[key].safeParse(raw[key]);
|
|
4031
4584
|
return parsed.success ? parsed.data : MEDIA_DEFAULTS[key];
|
|
4032
4585
|
};
|
|
4033
|
-
return {
|
|
4586
|
+
return {
|
|
4587
|
+
cropPadding: pick("cropPadding"),
|
|
4588
|
+
saveThumbnails: pick("saveThumbnails"),
|
|
4589
|
+
snapshotIntervalMs: pick("snapshotIntervalMs")
|
|
4590
|
+
};
|
|
4034
4591
|
}
|
|
4035
4592
|
//#endregion
|
|
4036
4593
|
//#region src/pipeline-analytics/store/identity-store.ts
|
|
@@ -4361,6 +4918,14 @@ var FACE_COLUMNS = [
|
|
|
4361
4918
|
{
|
|
4362
4919
|
name: "assignedSampleId",
|
|
4363
4920
|
type: "TEXT"
|
|
4921
|
+
},
|
|
4922
|
+
{
|
|
4923
|
+
name: "keyFrameMediaKey",
|
|
4924
|
+
type: "TEXT"
|
|
4925
|
+
},
|
|
4926
|
+
{
|
|
4927
|
+
name: "faceBbox",
|
|
4928
|
+
type: "JSON"
|
|
4364
4929
|
}
|
|
4365
4930
|
];
|
|
4366
4931
|
var FACE_INDEXES = [{
|
|
@@ -4433,7 +4998,9 @@ var FaceStore = class {
|
|
|
4433
4998
|
assigned: Boolean(r.data.assigned),
|
|
4434
4999
|
mediaKey: data.mediaKey ?? void 0,
|
|
4435
5000
|
recognizedIdentityId: data.recognizedIdentityId ?? void 0,
|
|
4436
|
-
assignedSampleId: data.assignedSampleId ?? void 0
|
|
5001
|
+
assignedSampleId: data.assignedSampleId ?? void 0,
|
|
5002
|
+
keyFrameMediaKey: data.keyFrameMediaKey ?? void 0,
|
|
5003
|
+
faceBbox: data.faceBbox ?? void 0
|
|
4437
5004
|
};
|
|
4438
5005
|
}).filter((f) => !f.assigned);
|
|
4439
5006
|
}
|
|
@@ -4597,8 +5164,11 @@ var FaceStore = class {
|
|
|
4597
5164
|
id: faceId,
|
|
4598
5165
|
...data,
|
|
4599
5166
|
assigned: Boolean(raw.assigned),
|
|
5167
|
+
mediaKey: data.mediaKey ?? void 0,
|
|
4600
5168
|
recognizedIdentityId: data.recognizedIdentityId ?? void 0,
|
|
4601
|
-
assignedSampleId: data.assignedSampleId ?? void 0
|
|
5169
|
+
assignedSampleId: data.assignedSampleId ?? void 0,
|
|
5170
|
+
keyFrameMediaKey: data.keyFrameMediaKey ?? void 0,
|
|
5171
|
+
faceBbox: data.faceBbox ?? void 0
|
|
4602
5172
|
};
|
|
4603
5173
|
}
|
|
4604
5174
|
/**
|
|
@@ -4642,7 +5212,9 @@ var FaceStore = class {
|
|
|
4642
5212
|
assigned: Boolean(r.data.assigned),
|
|
4643
5213
|
mediaKey: data.mediaKey ?? void 0,
|
|
4644
5214
|
recognizedIdentityId: data.recognizedIdentityId ?? void 0,
|
|
4645
|
-
assignedSampleId: data.assignedSampleId ?? void 0
|
|
5215
|
+
assignedSampleId: data.assignedSampleId ?? void 0,
|
|
5216
|
+
keyFrameMediaKey: data.keyFrameMediaKey ?? void 0,
|
|
5217
|
+
faceBbox: data.faceBbox ?? void 0
|
|
4646
5218
|
};
|
|
4647
5219
|
});
|
|
4648
5220
|
const filterMode = input.filter ?? "all";
|
|
@@ -4707,6 +5279,10 @@ var OBJECT_EMBEDDING_COLUMNS = [
|
|
|
4707
5279
|
{
|
|
4708
5280
|
name: "mediaKey",
|
|
4709
5281
|
type: "TEXT"
|
|
5282
|
+
},
|
|
5283
|
+
{
|
|
5284
|
+
name: "keyFrameMediaKey",
|
|
5285
|
+
type: "TEXT"
|
|
4710
5286
|
}
|
|
4711
5287
|
];
|
|
4712
5288
|
var ObjectEmbeddingStore = class {
|
|
@@ -4753,7 +5329,8 @@ var ObjectEmbeddingStore = class {
|
|
|
4753
5329
|
modelId: input.modelId,
|
|
4754
5330
|
dim: input.embedding.length,
|
|
4755
5331
|
confidence: input.confidence,
|
|
4756
|
-
...input.mediaKey !== void 0 ? { mediaKey: input.mediaKey } : {}
|
|
5332
|
+
...input.mediaKey !== void 0 ? { mediaKey: input.mediaKey } : {},
|
|
5333
|
+
...input.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: input.keyFrameMediaKey } : {}
|
|
4757
5334
|
};
|
|
4758
5335
|
try {
|
|
4759
5336
|
await this.store.set.mutate({
|
|
@@ -4791,7 +5368,8 @@ var ObjectEmbeddingStore = class {
|
|
|
4791
5368
|
return {
|
|
4792
5369
|
id: r.id,
|
|
4793
5370
|
...data,
|
|
4794
|
-
mediaKey: data.mediaKey ?? void 0
|
|
5371
|
+
mediaKey: data.mediaKey ?? void 0,
|
|
5372
|
+
keyFrameMediaKey: data.keyFrameMediaKey ?? void 0
|
|
4795
5373
|
};
|
|
4796
5374
|
});
|
|
4797
5375
|
}
|
|
@@ -4920,12 +5498,22 @@ function updateTrackAggregate(prev, match, opts) {
|
|
|
4920
5498
|
}
|
|
4921
5499
|
//#endregion
|
|
4922
5500
|
//#region src/pipeline-analytics/face-recognizer.ts
|
|
5501
|
+
/** At most one "dropping imageless track" log per this interval, per recognizer. */
|
|
5502
|
+
var FACE_IMAGELESS_LOG_THROTTLE_MS = 6e4;
|
|
4923
5503
|
var FaceRecognizer = class {
|
|
4924
5504
|
deps;
|
|
4925
5505
|
gallery = [];
|
|
4926
5506
|
names = /* @__PURE__ */ new Map();
|
|
4927
5507
|
aggregates = /* @__PURE__ */ new Map();
|
|
4928
5508
|
bestFace = /* @__PURE__ */ new Map();
|
|
5509
|
+
/** The ONE best-detection-per-track policy, shared with the best-frame path
|
|
5510
|
+
* (`index.ts`). Face params: no hysteresis / no rate-limit — always keep the
|
|
5511
|
+
* true highest-confidence face (holding a buffer in memory is cheap, and a
|
|
5512
|
+
* track may last well under the best-frame rate-limit window). */
|
|
5513
|
+
bestTracker = new BestDetectionTracker();
|
|
5514
|
+
/** Throttle for the "dropping imageless track" log — one line per minute at
|
|
5515
|
+
* most, so a busy scene that never produces a face crop can't flood logs. */
|
|
5516
|
+
lastImagelessLogAt = 0;
|
|
4929
5517
|
constructor(deps) {
|
|
4930
5518
|
this.deps = deps;
|
|
4931
5519
|
}
|
|
@@ -4959,55 +5547,25 @@ var FaceRecognizer = class {
|
|
|
4959
5547
|
threshold: settings.similarityThreshold,
|
|
4960
5548
|
margin: settings.margin
|
|
4961
5549
|
}) : /* @__PURE__ */ new Map();
|
|
5550
|
+
const labelWork = [];
|
|
4962
5551
|
for (const c of candidates) {
|
|
4963
5552
|
const match = matches.get(c.trackId) ?? null;
|
|
4964
5553
|
const { state, changed } = updateTrackAggregate(this.aggregates.get(c.trackId) ?? EMPTY_AGGREGATE, match, { confirmFrames: settings.confirmFrames });
|
|
4965
5554
|
this.aggregates.set(c.trackId, state);
|
|
4966
|
-
if (changed && state.assignedIdentityId !== null) {
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
trackId: c.trackId
|
|
4972
|
-
},
|
|
4973
|
-
meta: {
|
|
4974
|
-
identityId: state.assignedIdentityId,
|
|
4975
|
-
name: name ?? null,
|
|
4976
|
-
score: match?.score ?? null
|
|
4977
|
-
}
|
|
4978
|
-
});
|
|
4979
|
-
if (name !== void 0) {
|
|
4980
|
-
try {
|
|
4981
|
-
await this.deps.trackStore.setLabel(c.trackId, name);
|
|
4982
|
-
} catch (err) {
|
|
4983
|
-
this.deps.logger.warn("setLabel failed", {
|
|
4984
|
-
tags: { deviceId: input.deviceId },
|
|
4985
|
-
meta: {
|
|
4986
|
-
trackId: c.trackId,
|
|
4987
|
-
error: String(err)
|
|
4988
|
-
}
|
|
4989
|
-
});
|
|
4990
|
-
}
|
|
4991
|
-
try {
|
|
4992
|
-
await this.deps.eventStore.setLabelForTrack(c.trackId, name);
|
|
4993
|
-
} catch (err) {
|
|
4994
|
-
this.deps.logger.warn("setLabelForTrack failed", {
|
|
4995
|
-
tags: { deviceId: input.deviceId },
|
|
4996
|
-
meta: {
|
|
4997
|
-
trackId: c.trackId,
|
|
4998
|
-
error: String(err)
|
|
4999
|
-
}
|
|
5000
|
-
});
|
|
5001
|
-
}
|
|
5002
|
-
}
|
|
5003
|
-
}
|
|
5555
|
+
if (changed && state.assignedIdentityId !== null) labelWork.push({
|
|
5556
|
+
trackId: c.trackId,
|
|
5557
|
+
assignedIdentityId: state.assignedIdentityId,
|
|
5558
|
+
matchScore: match?.score ?? null
|
|
5559
|
+
});
|
|
5004
5560
|
const recognizedIdentityId = state.assignedIdentityId ?? match?.identityId ?? void 0;
|
|
5005
5561
|
const held = this.bestFace.get(c.trackId);
|
|
5006
|
-
|
|
5562
|
+
const isNewBest = this.bestTracker.observe(c.trackId, c.confidence, input.timestamp);
|
|
5563
|
+
const needsCrop = held !== void 0 && held.crop === void 0;
|
|
5564
|
+
if (isNewBest || needsCrop) {
|
|
5007
5565
|
const cropBbox = c.faceBbox ?? c.bbox;
|
|
5008
5566
|
let crop;
|
|
5009
|
-
if (input.frameHandle !== void 0) try {
|
|
5010
|
-
crop = await this.deps.captureCrop(input.frameHandle,
|
|
5567
|
+
if (c.faceBbox !== void 0 && input.frameHandle !== void 0) try {
|
|
5568
|
+
crop = await this.deps.captureCrop(input.frameHandle, c.faceBbox, input.frameWidth, input.frameHeight, input.cropPadding) ?? void 0;
|
|
5011
5569
|
} catch (err) {
|
|
5012
5570
|
this.deps.logger.debug("FaceRecognizer crop capture failed", {
|
|
5013
5571
|
tags: { deviceId: input.deviceId },
|
|
@@ -5017,14 +5575,20 @@ var FaceRecognizer = class {
|
|
|
5017
5575
|
}
|
|
5018
5576
|
});
|
|
5019
5577
|
}
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5578
|
+
if (isNewBest) {
|
|
5579
|
+
const bestCrop = crop ?? held?.crop;
|
|
5580
|
+
this.bestFace.set(c.trackId, {
|
|
5581
|
+
score: c.confidence,
|
|
5582
|
+
embedding: c.embedding,
|
|
5583
|
+
embeddingModelId: c.embeddingModelId,
|
|
5584
|
+
bbox: cropBbox,
|
|
5585
|
+
timestamp: input.timestamp,
|
|
5586
|
+
...bestCrop !== void 0 ? { crop: bestCrop } : {},
|
|
5587
|
+
...recognizedIdentityId !== void 0 ? { recognizedIdentityId } : {}
|
|
5588
|
+
});
|
|
5589
|
+
} else if (crop !== void 0 && held !== void 0) this.bestFace.set(c.trackId, {
|
|
5590
|
+
...held,
|
|
5591
|
+
crop
|
|
5028
5592
|
});
|
|
5029
5593
|
this.deps.logger.debug("face: best-face held", {
|
|
5030
5594
|
tags: {
|
|
@@ -5033,15 +5597,64 @@ var FaceRecognizer = class {
|
|
|
5033
5597
|
},
|
|
5034
5598
|
meta: {
|
|
5035
5599
|
score: c.confidence,
|
|
5036
|
-
|
|
5600
|
+
isNewBest,
|
|
5601
|
+
hasCrop: this.bestFace.get(c.trackId)?.crop !== void 0,
|
|
5037
5602
|
recognizedIdentityId: recognizedIdentityId ?? null
|
|
5038
5603
|
}
|
|
5039
5604
|
});
|
|
5040
|
-
} else if (recognizedIdentityId !== void 0 && held.recognizedIdentityId === void 0) this.bestFace.set(c.trackId, {
|
|
5605
|
+
} else if (held !== void 0 && recognizedIdentityId !== void 0 && held.recognizedIdentityId === void 0) this.bestFace.set(c.trackId, {
|
|
5041
5606
|
...held,
|
|
5042
5607
|
recognizedIdentityId
|
|
5043
5608
|
});
|
|
5044
5609
|
}
|
|
5610
|
+
for (const work of labelWork) {
|
|
5611
|
+
const name = this.names.get(work.assignedIdentityId);
|
|
5612
|
+
this.deps.logger.info("face: identity assigned", {
|
|
5613
|
+
tags: {
|
|
5614
|
+
deviceId: input.deviceId,
|
|
5615
|
+
trackId: work.trackId
|
|
5616
|
+
},
|
|
5617
|
+
meta: {
|
|
5618
|
+
identityId: work.assignedIdentityId,
|
|
5619
|
+
name: name ?? null,
|
|
5620
|
+
score: work.matchScore
|
|
5621
|
+
}
|
|
5622
|
+
});
|
|
5623
|
+
if (name === void 0) continue;
|
|
5624
|
+
try {
|
|
5625
|
+
await this.deps.trackStore.setLabel(work.trackId, name);
|
|
5626
|
+
} catch (err) {
|
|
5627
|
+
this.deps.logger.warn("setLabel failed", {
|
|
5628
|
+
tags: { deviceId: input.deviceId },
|
|
5629
|
+
meta: {
|
|
5630
|
+
trackId: work.trackId,
|
|
5631
|
+
error: String(err)
|
|
5632
|
+
}
|
|
5633
|
+
});
|
|
5634
|
+
}
|
|
5635
|
+
try {
|
|
5636
|
+
await this.deps.eventStore.setLabelForTrack(work.trackId, name);
|
|
5637
|
+
} catch (err) {
|
|
5638
|
+
this.deps.logger.warn("setLabelForTrack failed", {
|
|
5639
|
+
tags: { deviceId: input.deviceId },
|
|
5640
|
+
meta: {
|
|
5641
|
+
trackId: work.trackId,
|
|
5642
|
+
error: String(err)
|
|
5643
|
+
}
|
|
5644
|
+
});
|
|
5645
|
+
}
|
|
5646
|
+
try {
|
|
5647
|
+
await this.deps.recomputeImportance?.(work.trackId);
|
|
5648
|
+
} catch (err) {
|
|
5649
|
+
this.deps.logger.warn("recomputeImportance failed", {
|
|
5650
|
+
tags: { deviceId: input.deviceId },
|
|
5651
|
+
meta: {
|
|
5652
|
+
trackId: work.trackId,
|
|
5653
|
+
error: String(err)
|
|
5654
|
+
}
|
|
5655
|
+
});
|
|
5656
|
+
}
|
|
5657
|
+
}
|
|
5045
5658
|
}
|
|
5046
5659
|
/**
|
|
5047
5660
|
* Persist the held best face for a finished track as ONE FaceStore buffer
|
|
@@ -5052,6 +5665,7 @@ var FaceRecognizer = class {
|
|
|
5052
5665
|
const held = this.bestFace.get(trackId);
|
|
5053
5666
|
this.aggregates.delete(trackId);
|
|
5054
5667
|
this.bestFace.delete(trackId);
|
|
5668
|
+
this.bestTracker.delete(trackId);
|
|
5055
5669
|
if (held === void 0) {
|
|
5056
5670
|
this.deps.logger.debug("face: track ended without a held face", { tags: {
|
|
5057
5671
|
deviceId,
|
|
@@ -5059,9 +5673,23 @@ var FaceRecognizer = class {
|
|
|
5059
5673
|
} });
|
|
5060
5674
|
return;
|
|
5061
5675
|
}
|
|
5676
|
+
if (held.crop === void 0) {
|
|
5677
|
+
const now = Date.now();
|
|
5678
|
+
if (now - this.lastImagelessLogAt >= FACE_IMAGELESS_LOG_THROTTLE_MS) {
|
|
5679
|
+
this.lastImagelessLogAt = now;
|
|
5680
|
+
this.deps.logger.info("face: dropping imageless track (no crop captured)", {
|
|
5681
|
+
tags: {
|
|
5682
|
+
deviceId,
|
|
5683
|
+
trackId
|
|
5684
|
+
},
|
|
5685
|
+
meta: { score: held.score }
|
|
5686
|
+
});
|
|
5687
|
+
}
|
|
5688
|
+
return;
|
|
5689
|
+
}
|
|
5062
5690
|
const faceId = `face-${trackId}`;
|
|
5063
5691
|
let mediaKey;
|
|
5064
|
-
|
|
5692
|
+
try {
|
|
5065
5693
|
mediaKey = await this.deps.mediaStore.put({
|
|
5066
5694
|
deviceId,
|
|
5067
5695
|
ownerKind: "face",
|
|
@@ -5079,6 +5707,17 @@ var FaceRecognizer = class {
|
|
|
5079
5707
|
}
|
|
5080
5708
|
});
|
|
5081
5709
|
}
|
|
5710
|
+
if (mediaKey === void 0) {
|
|
5711
|
+
this.deps.logger.warn("face: crop store failed — dropping face row", {
|
|
5712
|
+
tags: {
|
|
5713
|
+
deviceId,
|
|
5714
|
+
trackId
|
|
5715
|
+
},
|
|
5716
|
+
meta: { faceId }
|
|
5717
|
+
});
|
|
5718
|
+
return;
|
|
5719
|
+
}
|
|
5720
|
+
const keyFrameMediaKey = this.deps.getKeyFrameMediaKey?.(trackId);
|
|
5082
5721
|
try {
|
|
5083
5722
|
await this.deps.faceStore.insert({
|
|
5084
5723
|
id: faceId,
|
|
@@ -5086,9 +5725,11 @@ var FaceRecognizer = class {
|
|
|
5086
5725
|
trackId,
|
|
5087
5726
|
timestamp: held.timestamp,
|
|
5088
5727
|
embedding: held.embedding,
|
|
5089
|
-
|
|
5728
|
+
mediaKey,
|
|
5090
5729
|
...held.recognizedIdentityId !== void 0 ? { recognizedIdentityId: held.recognizedIdentityId } : {},
|
|
5091
|
-
assigned: false
|
|
5730
|
+
assigned: false,
|
|
5731
|
+
faceBbox: held.bbox,
|
|
5732
|
+
...keyFrameMediaKey !== void 0 ? { keyFrameMediaKey } : {}
|
|
5092
5733
|
});
|
|
5093
5734
|
this.deps.logger.info("face: buffered to gallery", {
|
|
5094
5735
|
tags: {
|
|
@@ -5097,7 +5738,7 @@ var FaceRecognizer = class {
|
|
|
5097
5738
|
},
|
|
5098
5739
|
meta: {
|
|
5099
5740
|
faceId,
|
|
5100
|
-
hasCrop:
|
|
5741
|
+
hasCrop: true,
|
|
5101
5742
|
recognizedIdentityId: held.recognizedIdentityId ?? null,
|
|
5102
5743
|
score: held.score
|
|
5103
5744
|
}
|
|
@@ -5429,6 +6070,38 @@ var PlateRecognizer = class {
|
|
|
5429
6070
|
}
|
|
5430
6071
|
};
|
|
5431
6072
|
//#endregion
|
|
6073
|
+
//#region src/shared/frame/shared-frame-resolver.ts
|
|
6074
|
+
function frameHandleKey(h) {
|
|
6075
|
+
return `${h.nodeId}:${h.shmId}:${h.slot}:${h.seq}`;
|
|
6076
|
+
}
|
|
6077
|
+
function createSharedFrameResolver(resolve) {
|
|
6078
|
+
let cache = null;
|
|
6079
|
+
return (handle) => {
|
|
6080
|
+
const key = frameHandleKey(handle);
|
|
6081
|
+
if (cache === null || cache.key !== key) cache = {
|
|
6082
|
+
key,
|
|
6083
|
+
value: resolve(handle)
|
|
6084
|
+
};
|
|
6085
|
+
return cache.value;
|
|
6086
|
+
};
|
|
6087
|
+
}
|
|
6088
|
+
//#endregion
|
|
6089
|
+
//#region src/shared/frame/encode-crop.ts
|
|
6090
|
+
/**
|
|
6091
|
+
* JPEG-encode an already-cropped raw RGB (24-bit) buffer. Used for the
|
|
6092
|
+
* NATIVE-resolution crop path: the decode worker returns the exact ROI pixels
|
|
6093
|
+
* at native res, so there is nothing left to crop — only to encode to the same
|
|
6094
|
+
* JPEG contract `extractCrop` produces (quality 90). Kept separate from
|
|
6095
|
+
* `extractCrop` (which crops a FULL frame) so the native path never re-crops.
|
|
6096
|
+
*/
|
|
6097
|
+
async function encodeRgbCropToJpeg(bytes, width, height) {
|
|
6098
|
+
return (0, sharp.default)(Buffer.from(bytes), { raw: {
|
|
6099
|
+
width,
|
|
6100
|
+
height,
|
|
6101
|
+
channels: 3
|
|
6102
|
+
} }).jpeg({ quality: 90 }).toBuffer();
|
|
6103
|
+
}
|
|
6104
|
+
//#endregion
|
|
5432
6105
|
//#region src/pipeline-analytics/pipeline/event-child-crops.ts
|
|
5433
6106
|
/**
|
|
5434
6107
|
* Compute Intersection-over-Union between two (x,y,w,h) bounding boxes.
|
|
@@ -5727,6 +6400,24 @@ function createEventMediaHandler(deps) {
|
|
|
5727
6400
|
*/
|
|
5728
6401
|
var TTL_SWEEP_INTERVAL_MS = 5e3;
|
|
5729
6402
|
var SETTINGS_CACHE_TTL_MS = 5e3;
|
|
6403
|
+
/** §5 best-frame: a track's `thumbnail` is overwritten only when the current
|
|
6404
|
+
* detection confidence beats the held best by at least this margin (hysteresis
|
|
6405
|
+
* so jitter around a plateau doesn't churn the write). */
|
|
6406
|
+
var BEST_FRAME_HYSTERESIS = .05;
|
|
6407
|
+
/** §5 best-frame: at most one best-thumbnail capture per this interval per track. */
|
|
6408
|
+
var BEST_FRAME_MIN_GAP_MS = 2e3;
|
|
6409
|
+
/** Design B: cap (px) on the width of the native KEY FRAME (full-frame native
|
|
6410
|
+
* capture). Native resolution is the point, but a full 4K RGB surface over the
|
|
6411
|
+
* transport per new-best is wasteful for a web detail view — 1920px keeps a
|
|
6412
|
+
* sharp native frame while bounding the copy (a miss falls back to the
|
|
6413
|
+
* detection-res frame, which is already ≤640px). */
|
|
6414
|
+
var KEYFRAME_NATIVE_MAX_WIDTH = 1920;
|
|
6415
|
+
/** getKeyEvents: max completed tracks pulled from a window before importance
|
|
6416
|
+
* ranking. Ordering is by importance (not firstSeen) and legacy rows score on
|
|
6417
|
+
* read, so we over-fetch candidates and trim to `limit` after sorting. */
|
|
6418
|
+
var KEY_EVENT_CANDIDATE_CAP = 500;
|
|
6419
|
+
/** getKeyEvents: default page size when the caller omits `limit`. */
|
|
6420
|
+
var KEY_EVENT_DEFAULT_LIMIT = 50;
|
|
5730
6421
|
/** Cluster setting key (in the centralized addon store) selecting the SINGLE
|
|
5731
6422
|
* node that runs post-analysis (event/media/audio/motion generation). All
|
|
5732
6423
|
* other nodes are fully inert. No multi-node balancing. Default: the hub. */
|
|
@@ -5838,6 +6529,31 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
5838
6529
|
mediaCacheByDevice = /* @__PURE__ */ new Map();
|
|
5839
6530
|
/** Consecutive detector-dropout frames skipped per (deviceId, source) key. */
|
|
5840
6531
|
dropoutSkipsByKey = /* @__PURE__ */ new Map();
|
|
6532
|
+
/** Best (highest-confidence) frame per track — drives the single overwrite
|
|
6533
|
+
* `thumbnail` (§5 best-frame). Shares the ONE best-detection policy with the
|
|
6534
|
+
* face path (`face-recognizer.ts`); rate-limited here since each best-frame
|
|
6535
|
+
* capture re-encodes a full boxed frame. */
|
|
6536
|
+
bestFrameTracker = new BestDetectionTracker({
|
|
6537
|
+
hysteresis: BEST_FRAME_HYSTERESIS,
|
|
6538
|
+
minGapMs: BEST_FRAME_MIN_GAP_MS
|
|
6539
|
+
});
|
|
6540
|
+
/** Best (highest-confidence) CLIP-object detection per track — drives ONE
|
|
6541
|
+
* tight object-crop capture whose media key is written onto the embedding
|
|
6542
|
+
* row (so a semantic-search hit's thumbnail IS the embedded crop). Shares the
|
|
6543
|
+
* unified best-per-track decision (`TrackBestSelector`); the persistent
|
|
6544
|
+
* cross-restart "best" gate stays in `ObjectEmbeddingStore.upsertIfBetter`. */
|
|
6545
|
+
objectEmbeddingBestSelector = new TrackBestSelector();
|
|
6546
|
+
/** Design B: the track's shared native key-frame media key, captured at the
|
|
6547
|
+
* best-detection moment (object-embedding best path). Read by the face path
|
|
6548
|
+
* at track end so a face row links to the SAME single key frame. Cleared on
|
|
6549
|
+
* track end. */
|
|
6550
|
+
keyFrameKeyByTrackId = /* @__PURE__ */ new Map();
|
|
6551
|
+
/** The shared crop extractor (native-res first, detection-frame fallback),
|
|
6552
|
+
* captured in the constructor so `processFrame` can crop object thumbnails in
|
|
6553
|
+
* the same live-frame window as the face/plate/event-media captures. The
|
|
6554
|
+
* optional `maxWidth` caps the native crop width (used for the full-frame key
|
|
6555
|
+
* frame so a 4K native surface never floods the transport). */
|
|
6556
|
+
captureCrop = null;
|
|
5841
6557
|
shuttingDown = false;
|
|
5842
6558
|
/** True only on the cluster's designated post-processing node. When false the
|
|
5843
6559
|
* addon subscribes to NOTHING — fully inert (no event/media generation). */
|
|
@@ -5871,7 +6587,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
5871
6587
|
let storage = this.ctx.kernel.storage;
|
|
5872
6588
|
const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
|
|
5873
6589
|
if (mediaRoot) {
|
|
5874
|
-
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-
|
|
6590
|
+
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-Cg_cGqs0.js"));
|
|
5875
6591
|
storage = new FilesystemStorageProvider(mediaRoot);
|
|
5876
6592
|
logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
|
|
5877
6593
|
}
|
|
@@ -5937,23 +6653,62 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
5937
6653
|
});
|
|
5938
6654
|
const ownNodeIdForFaces = ownNodeId;
|
|
5939
6655
|
const frameReadersForFaces = this.frameReaders;
|
|
5940
|
-
const
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
6656
|
+
const pipelineRunnerApi = api.pipelineRunner;
|
|
6657
|
+
const cropMetricLogger = logger.child("NativeCrop");
|
|
6658
|
+
let nativeHits = 0;
|
|
6659
|
+
let nativeFallbacks = 0;
|
|
6660
|
+
let lastCropMetricAt = 0;
|
|
6661
|
+
const NATIVE_CROP_METRIC_INTERVAL_MS = 3e4;
|
|
6662
|
+
const bumpCropMetric = (hit) => {
|
|
6663
|
+
if (hit) nativeHits += 1;
|
|
6664
|
+
else nativeFallbacks += 1;
|
|
6665
|
+
const now = Date.now();
|
|
6666
|
+
if (now - lastCropMetricAt < NATIVE_CROP_METRIC_INTERVAL_MS) return;
|
|
6667
|
+
lastCropMetricAt = now;
|
|
6668
|
+
cropMetricLogger.info("native-crop window", { meta: {
|
|
6669
|
+
nativeHits,
|
|
6670
|
+
detectionFrameFallbacks: nativeFallbacks
|
|
6671
|
+
} });
|
|
6672
|
+
};
|
|
6673
|
+
const tryNativeCrop = async (frameHandle, paddedNorm, maxWidth) => {
|
|
6674
|
+
if (!pipelineRunnerApi?.getNativeCrop) return null;
|
|
6675
|
+
try {
|
|
6676
|
+
const native = await pipelineRunnerApi.getNativeCrop.query({
|
|
6677
|
+
handle: frameHandle,
|
|
6678
|
+
bbox: paddedNorm,
|
|
6679
|
+
...maxWidth !== void 0 ? { maxWidth } : {}
|
|
6680
|
+
}, require_dist.nodePin(frameHandle.nodeId));
|
|
6681
|
+
if (!native || native.width <= 0 || native.height <= 0) return null;
|
|
6682
|
+
return await encodeRgbCropToJpeg(Buffer.from(native.bytes), native.width, native.height);
|
|
6683
|
+
} catch (err) {
|
|
6684
|
+
cropMetricLogger.debug("native crop request failed — falling back", { meta: { error: require_dist.errMsg(err) } });
|
|
6685
|
+
return null;
|
|
6686
|
+
}
|
|
6687
|
+
};
|
|
6688
|
+
const resolveFrameShared = createSharedFrameResolver((frameHandle) => require_resolve_frame.resolveFrame(frameHandle, {
|
|
6689
|
+
ownNodeId: ownNodeIdForFaces,
|
|
6690
|
+
readers: frameReadersForFaces,
|
|
6691
|
+
getRemoteFrame
|
|
6692
|
+
}));
|
|
6693
|
+
const captureCrop = async (frameHandle, bbox, frameWidth, frameHeight, padding, maxWidth) => {
|
|
5948
6694
|
const paddedNorm = padBbox({
|
|
5949
6695
|
x: bbox.x / frameWidth,
|
|
5950
6696
|
y: bbox.y / frameHeight,
|
|
5951
6697
|
w: bbox.w / frameWidth,
|
|
5952
6698
|
h: bbox.h / frameHeight
|
|
5953
6699
|
}, padding);
|
|
5954
|
-
const
|
|
6700
|
+
const nativeCrop = await tryNativeCrop(frameHandle, paddedNorm, maxWidth);
|
|
6701
|
+
if (nativeCrop) {
|
|
6702
|
+
bumpCropMetric(true);
|
|
6703
|
+
return nativeCrop;
|
|
6704
|
+
}
|
|
6705
|
+
bumpCropMetric(false);
|
|
6706
|
+
const decoded = await resolveFrameShared(frameHandle);
|
|
6707
|
+
if (!decoded || decoded.format !== "rgb") return null;
|
|
6708
|
+
const { crop } = await require_resolve_frame.extractCrop(Buffer.isBuffer(decoded.data) ? decoded.data : Buffer.from(decoded.data), decoded.width, decoded.height, paddedNorm);
|
|
5955
6709
|
return crop;
|
|
5956
6710
|
};
|
|
6711
|
+
this.captureCrop = captureCrop;
|
|
5957
6712
|
this.faceRecognizer = new FaceRecognizer({
|
|
5958
6713
|
identityStore: this.identityStore,
|
|
5959
6714
|
faceStore: this.faceStore,
|
|
@@ -5961,6 +6716,16 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
5961
6716
|
trackStore: this.trackStore,
|
|
5962
6717
|
eventStore: this.eventStore,
|
|
5963
6718
|
captureCrop,
|
|
6719
|
+
recomputeImportance: (trackId) => {
|
|
6720
|
+
const trackStore = this.trackStore;
|
|
6721
|
+
const eventStore = this.eventStore;
|
|
6722
|
+
if (!trackStore || !eventStore) return Promise.resolve();
|
|
6723
|
+
return recomputeTrackImportance({
|
|
6724
|
+
trackStore,
|
|
6725
|
+
eventStore
|
|
6726
|
+
}, trackId);
|
|
6727
|
+
},
|
|
6728
|
+
getKeyFrameMediaKey: (trackId) => this.keyFrameKeyByTrackId.get(trackId),
|
|
5964
6729
|
logger: logger.child("FaceRecognizer")
|
|
5965
6730
|
});
|
|
5966
6731
|
this.faceRecognizer.refreshGallery();
|
|
@@ -5998,7 +6763,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
5998
6763
|
try {
|
|
5999
6764
|
const handler = createEventMediaHandler({ getMedia: async (id) => {
|
|
6000
6765
|
try {
|
|
6001
|
-
return await this.
|
|
6766
|
+
return await this.readMediaByEventOrKey(id);
|
|
6002
6767
|
} catch (err) {
|
|
6003
6768
|
this.ctx.logger.warn("readEventThumbnail failed", { meta: {
|
|
6004
6769
|
eventId: id,
|
|
@@ -6381,6 +7146,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6381
7146
|
this.processors.clear();
|
|
6382
7147
|
this.lastActiveTrackIds.clear();
|
|
6383
7148
|
this.dropoutSkipsByKey.clear();
|
|
7149
|
+
this.bestFrameTracker.clear();
|
|
7150
|
+
this.objectEmbeddingBestSelector.clear();
|
|
6384
7151
|
this.levelStateByDevice.clear();
|
|
6385
7152
|
this.settingsCacheByDevice.clear();
|
|
6386
7153
|
this.sensitivityCacheByDevice.clear();
|
|
@@ -6487,12 +7254,15 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6487
7254
|
className: t.className,
|
|
6488
7255
|
source
|
|
6489
7256
|
} });
|
|
6490
|
-
if (this.eventMediaDispatcher && frameHandle)
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
7257
|
+
if (this.eventMediaDispatcher && frameHandle) {
|
|
7258
|
+
firstFrameTargets.push({
|
|
7259
|
+
trackId: id,
|
|
7260
|
+
timestamp: result.timestamp,
|
|
7261
|
+
bbox: { ...t.bbox },
|
|
7262
|
+
...t.label ? { label: t.label } : {}
|
|
7263
|
+
});
|
|
7264
|
+
this.trackStore.seedSnapshotClock(id, result.timestamp);
|
|
7265
|
+
}
|
|
6496
7266
|
this.ctx.eventBus.emit({
|
|
6497
7267
|
id: `pa-${(0, node_crypto.randomUUID)()}`,
|
|
6498
7268
|
timestamp: new Date(result.timestamp),
|
|
@@ -6533,16 +7303,18 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6533
7303
|
} });
|
|
6534
7304
|
}
|
|
6535
7305
|
await Promise.all(result.objectEvents.map((e) => this.eventStore.insertObject(e)));
|
|
6536
|
-
|
|
6537
|
-
|
|
7306
|
+
const objectEmbeddingBests = [];
|
|
7307
|
+
if (this.objectEmbeddingStore) for (const t of result.tracked) {
|
|
7308
|
+
if (!isClipObjectEmbedding(t)) continue;
|
|
7309
|
+
if (this.objectEmbeddingBestSelector.observe({
|
|
6538
7310
|
trackId: t.trackId,
|
|
6539
|
-
|
|
6540
|
-
|
|
7311
|
+
confidence: t.confidence,
|
|
7312
|
+
atMs: result.timestamp,
|
|
6541
7313
|
className: t.className,
|
|
7314
|
+
bbox: t.bbox,
|
|
6542
7315
|
embedding: t.embedding,
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
});
|
|
7316
|
+
embeddingModelId: t.embeddingModelId
|
|
7317
|
+
})) objectEmbeddingBests.push(t);
|
|
6546
7318
|
}
|
|
6547
7319
|
const faceSettings = this.faceRecognizer ? await this.resolveDeviceFaceSettings(deviceId) : null;
|
|
6548
7320
|
const faceGloballyEnabled = this.faceRecognizer ? await this.resolveGlobalFaceEnabled() : false;
|
|
@@ -6565,11 +7337,13 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6565
7337
|
let plateCrops = 0;
|
|
6566
7338
|
for (const crops of childCropsByEvent.values()) for (const c of crops) if (c.kind === "faceCrop") faceCrops += 1;
|
|
6567
7339
|
else plateCrops += 1;
|
|
6568
|
-
|
|
7340
|
+
const snapshotTargets = this.buildSnapshotTargets(result.tracked, result.timestamp, mediaSettings);
|
|
7341
|
+
if (eventTargets.length > 0 || firstFrameTargets.length > 0 || snapshotTargets.length > 0) {
|
|
6569
7342
|
log.info("media capture", { meta: {
|
|
6570
7343
|
source,
|
|
6571
7344
|
events: eventTargets.length,
|
|
6572
7345
|
trackFrames: firstFrameTargets.length,
|
|
7346
|
+
snapshots: snapshotTargets.length,
|
|
6573
7347
|
faceCrops,
|
|
6574
7348
|
plateCrops
|
|
6575
7349
|
} });
|
|
@@ -6578,8 +7352,20 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6578
7352
|
frameHandle,
|
|
6579
7353
|
events: eventTargets,
|
|
6580
7354
|
trackFrames: firstFrameTargets,
|
|
7355
|
+
snapshots: snapshotTargets,
|
|
6581
7356
|
cropPadding: mediaSettings.cropPadding
|
|
6582
|
-
})
|
|
7357
|
+
}).then((res) => {
|
|
7358
|
+
for (const s of res.storedSnapshots) this.trackStore?.addSnapshot(s.trackId, {
|
|
7359
|
+
timestamp: s.timestamp,
|
|
7360
|
+
position: {
|
|
7361
|
+
x: s.bbox.x + s.bbox.w / 2,
|
|
7362
|
+
y: s.bbox.y + s.bbox.h / 2,
|
|
7363
|
+
timestamp: s.timestamp,
|
|
7364
|
+
bbox: s.bbox
|
|
7365
|
+
},
|
|
7366
|
+
mediaKey: s.mediaKey
|
|
7367
|
+
});
|
|
7368
|
+
}).catch(() => {});
|
|
6583
7369
|
}
|
|
6584
7370
|
}
|
|
6585
7371
|
if (this.faceRecognizer && faceGloballyEnabled && faceSettings) this.faceRecognizer.processFrame({
|
|
@@ -6601,6 +7387,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6601
7387
|
cropPadding: mediaSettings.cropPadding,
|
|
6602
7388
|
...frameHandle !== void 0 ? { frameHandle } : {}
|
|
6603
7389
|
});
|
|
7390
|
+
if (objectEmbeddingBests.length > 0) this.persistObjectEmbeddingBests(deviceId, result.timestamp, objectEmbeddingBests, frameHandle, result.frameWidth, result.frameHeight, mediaSettings.cropPadding);
|
|
6604
7391
|
for (const e of result.objectEvents) this.ctx.eventBus.emit({
|
|
6605
7392
|
id: `pa-${e.id}`,
|
|
6606
7393
|
timestamp: new Date(e.timestamp),
|
|
@@ -6712,6 +7499,112 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6712
7499
|
});
|
|
6713
7500
|
return settings;
|
|
6714
7501
|
}
|
|
7502
|
+
/**
|
|
7503
|
+
* §5 — decide which active tracks need periodic media THIS frame. Pure over
|
|
7504
|
+
* TrackStore.lastSnapshotAt + the per-track best-confidence map:
|
|
7505
|
+
* • `snapshot` (append) + `lastFrame` (rolling overwrite) fire together on
|
|
7506
|
+
* the snapshotIntervalMs cadence, gated by `saveThumbnails`;
|
|
7507
|
+
* • `thumbnail` (best) fires when confidence beats the held best by the
|
|
7508
|
+
* hysteresis margin, rate-limited to one per BEST_FRAME_MIN_GAP_MS, and is
|
|
7509
|
+
* NOT gated by saveThumbnails (a single best still-frame is always useful).
|
|
7510
|
+
* The per-track best-confidence map is updated here as a side effect.
|
|
7511
|
+
*/
|
|
7512
|
+
/**
|
|
7513
|
+
* Capture the tight object crop for each new-best CLIP track (native-res via
|
|
7514
|
+
* the shared extractor) and upsert the embedding row with that crop's media
|
|
7515
|
+
* key. The crop uses `putReplacing` so exactly ONE object crop is kept per
|
|
7516
|
+
* track (the current peak). The embedding is upserted even when no crop was
|
|
7517
|
+
* captured (no frame handle) so semantic search still works — the crop just
|
|
7518
|
+
* enhances the search-hit thumbnail. `upsertIfBetter` remains the durable
|
|
7519
|
+
* cross-restart best gate (R3). Best-effort; issued in the live-frame window.
|
|
7520
|
+
*/
|
|
7521
|
+
async persistObjectEmbeddingBests(deviceId, timestamp, bests, frameHandle, frameWidth, frameHeight, cropPadding) {
|
|
7522
|
+
const store = this.objectEmbeddingStore;
|
|
7523
|
+
if (!store) return;
|
|
7524
|
+
await Promise.all(bests.map(async (t) => {
|
|
7525
|
+
if (!isClipObjectEmbedding(t)) return;
|
|
7526
|
+
let mediaKey;
|
|
7527
|
+
let keyFrameMediaKey;
|
|
7528
|
+
if (frameHandle !== void 0 && this.captureCrop && this.mediaStore) {
|
|
7529
|
+
try {
|
|
7530
|
+
const crop = await this.captureCrop(frameHandle, t.bbox, frameWidth, frameHeight, cropPadding);
|
|
7531
|
+
if (crop) mediaKey = await this.mediaStore.putReplacing({
|
|
7532
|
+
deviceId,
|
|
7533
|
+
ownerKind: "track",
|
|
7534
|
+
ownerId: t.trackId,
|
|
7535
|
+
kind: "crop",
|
|
7536
|
+
timestamp,
|
|
7537
|
+
data: crop
|
|
7538
|
+
});
|
|
7539
|
+
} catch (err) {
|
|
7540
|
+
this.ctx.logger.debug("object-embedding crop capture failed", {
|
|
7541
|
+
tags: { deviceId },
|
|
7542
|
+
meta: {
|
|
7543
|
+
trackId: t.trackId,
|
|
7544
|
+
error: require_dist.errMsg(err)
|
|
7545
|
+
}
|
|
7546
|
+
});
|
|
7547
|
+
}
|
|
7548
|
+
try {
|
|
7549
|
+
const keyFrame = await this.captureCrop(frameHandle, {
|
|
7550
|
+
x: 0,
|
|
7551
|
+
y: 0,
|
|
7552
|
+
w: frameWidth,
|
|
7553
|
+
h: frameHeight
|
|
7554
|
+
}, frameWidth, frameHeight, 0, KEYFRAME_NATIVE_MAX_WIDTH);
|
|
7555
|
+
if (keyFrame) {
|
|
7556
|
+
keyFrameMediaKey = await this.mediaStore.putReplacing({
|
|
7557
|
+
deviceId,
|
|
7558
|
+
ownerKind: "track",
|
|
7559
|
+
ownerId: t.trackId,
|
|
7560
|
+
kind: "keyFrame",
|
|
7561
|
+
timestamp,
|
|
7562
|
+
data: keyFrame
|
|
7563
|
+
});
|
|
7564
|
+
this.keyFrameKeyByTrackId.set(t.trackId, keyFrameMediaKey);
|
|
7565
|
+
}
|
|
7566
|
+
} catch (err) {
|
|
7567
|
+
this.ctx.logger.debug("key-frame capture failed", {
|
|
7568
|
+
tags: { deviceId },
|
|
7569
|
+
meta: {
|
|
7570
|
+
trackId: t.trackId,
|
|
7571
|
+
error: require_dist.errMsg(err)
|
|
7572
|
+
}
|
|
7573
|
+
});
|
|
7574
|
+
}
|
|
7575
|
+
}
|
|
7576
|
+
await store.upsertIfBetter({
|
|
7577
|
+
trackId: t.trackId,
|
|
7578
|
+
deviceId,
|
|
7579
|
+
timestamp,
|
|
7580
|
+
className: t.className,
|
|
7581
|
+
embedding: t.embedding,
|
|
7582
|
+
modelId: t.embeddingModelId,
|
|
7583
|
+
confidence: t.confidence,
|
|
7584
|
+
...mediaKey !== void 0 ? { mediaKey } : {},
|
|
7585
|
+
...keyFrameMediaKey !== void 0 ? { keyFrameMediaKey } : {}
|
|
7586
|
+
});
|
|
7587
|
+
}));
|
|
7588
|
+
}
|
|
7589
|
+
buildSnapshotTargets(tracked, timestamp, media) {
|
|
7590
|
+
const targets = [];
|
|
7591
|
+
for (const t of tracked) {
|
|
7592
|
+
const lastSnap = this.trackStore.lastSnapshotAt(t.trackId);
|
|
7593
|
+
const dueSnapshot = media.saveThumbnails && lastSnap > 0 && timestamp - lastSnap >= media.snapshotIntervalMs;
|
|
7594
|
+
const isNewBest = this.bestFrameTracker.observe(t.trackId, t.confidence, timestamp);
|
|
7595
|
+
if (!dueSnapshot && !isNewBest) continue;
|
|
7596
|
+
targets.push({
|
|
7597
|
+
trackId: t.trackId,
|
|
7598
|
+
timestamp,
|
|
7599
|
+
bbox: { ...t.bbox },
|
|
7600
|
+
...t.label ? { label: t.label } : {},
|
|
7601
|
+
appendSnapshot: dueSnapshot,
|
|
7602
|
+
rollingLastFrame: dueSnapshot,
|
|
7603
|
+
bestThumbnail: isNewBest
|
|
7604
|
+
});
|
|
7605
|
+
}
|
|
7606
|
+
return targets;
|
|
7607
|
+
}
|
|
6715
7608
|
async handleAudioResult(data) {
|
|
6716
7609
|
if (this.shuttingDown) return;
|
|
6717
7610
|
const { deviceId, frame } = data;
|
|
@@ -6964,8 +7857,36 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6964
7857
|
positions: t.positions.length
|
|
6965
7858
|
}
|
|
6966
7859
|
});
|
|
6967
|
-
this.faceRecognizer?.onTrackEnd(t.deviceId, t.trackId);
|
|
7860
|
+
const faceEnd = this.faceRecognizer?.onTrackEnd(t.deviceId, t.trackId);
|
|
7861
|
+
const dropKeyFrameKey = () => {
|
|
7862
|
+
this.keyFrameKeyByTrackId.delete(t.trackId);
|
|
7863
|
+
};
|
|
7864
|
+
if (faceEnd) faceEnd.finally(dropKeyFrameKey);
|
|
7865
|
+
else dropKeyFrameKey();
|
|
6968
7866
|
this.plateRecognizer?.onTrackEnd(t.deviceId, t.trackId);
|
|
7867
|
+
try {
|
|
7868
|
+
const peak = await this.eventStore?.peakForTrack(t.trackId);
|
|
7869
|
+
if (peak) {
|
|
7870
|
+
const { importance, reason } = computeImportance({
|
|
7871
|
+
peakConfidence: this.bestFrameTracker.peak(t.trackId)?.confidence ?? peak.peakConfidence,
|
|
7872
|
+
className: t.className,
|
|
7873
|
+
durationMs: duration,
|
|
7874
|
+
peakBboxAreaFrac: peak.peakBboxAreaFrac,
|
|
7875
|
+
totalDistance: t.totalDistance,
|
|
7876
|
+
zonesVisited: t.zonesVisited,
|
|
7877
|
+
...t.label !== void 0 ? { label: t.label } : {}
|
|
7878
|
+
});
|
|
7879
|
+
await this.trackStore?.setImportance(t.trackId, importance, reason, peak.bestEventId);
|
|
7880
|
+
if (peak.bestEventId !== void 0) await this.eventStore?.setImportanceForTrack(t.trackId, importance);
|
|
7881
|
+
}
|
|
7882
|
+
} catch (err) {
|
|
7883
|
+
this.ctx.logger.debug("importance scoring failed", { meta: {
|
|
7884
|
+
trackId: t.trackId,
|
|
7885
|
+
error: String(err)
|
|
7886
|
+
} });
|
|
7887
|
+
}
|
|
7888
|
+
this.bestFrameTracker.delete(t.trackId);
|
|
7889
|
+
this.objectEmbeddingBestSelector.delete(t.trackId);
|
|
6969
7890
|
this.ctx.eventBus.emit({
|
|
6970
7891
|
id: `pa-end-${t.trackId}`,
|
|
6971
7892
|
timestamp: new Date(t.lastSeen),
|
|
@@ -7188,6 +8109,32 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
7188
8109
|
if (input.projection !== "slim" || this.eventMediaBaseUrl === null) return events;
|
|
7189
8110
|
return this.withMediaUrl(events);
|
|
7190
8111
|
}
|
|
8112
|
+
async getKeyEvents(input) {
|
|
8113
|
+
const trackStore = this.trackStore;
|
|
8114
|
+
if (!trackStore) return [];
|
|
8115
|
+
const eventStore = this.eventStore;
|
|
8116
|
+
try {
|
|
8117
|
+
const candidates = await trackStore.queryHistorical({
|
|
8118
|
+
deviceId: input.deviceId,
|
|
8119
|
+
since: input.since,
|
|
8120
|
+
until: input.until,
|
|
8121
|
+
limit: KEY_EVENT_CANDIDATE_CAP
|
|
8122
|
+
});
|
|
8123
|
+
const peakLookup = (trackId) => eventStore ? eventStore.peakForTrack(trackId) : Promise.resolve({
|
|
8124
|
+
peakConfidence: 0,
|
|
8125
|
+
peakBboxAreaFrac: 0,
|
|
8126
|
+
bestEventId: void 0
|
|
8127
|
+
});
|
|
8128
|
+
return await rankKeyEvents(candidates, {
|
|
8129
|
+
limit: input.limit ?? KEY_EVENT_DEFAULT_LIMIT,
|
|
8130
|
+
...input.minImportance !== void 0 ? { minImportance: input.minImportance } : {},
|
|
8131
|
+
...input.classFilter !== void 0 ? { classFilter: input.classFilter } : {}
|
|
8132
|
+
}, peakLookup);
|
|
8133
|
+
} catch (err) {
|
|
8134
|
+
this.ctx.logger.debug("getKeyEvents failed", { meta: { error: String(err) } });
|
|
8135
|
+
return [];
|
|
8136
|
+
}
|
|
8137
|
+
}
|
|
7191
8138
|
async getAudioEvents(input) {
|
|
7192
8139
|
const events = await (this.eventStore?.queryAudio(input) ?? Promise.resolve([]));
|
|
7193
8140
|
if (input.projection !== "slim" || this.eventMediaBaseUrl === null) return events;
|
|
@@ -7230,6 +8177,12 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
7230
8177
|
className: input.classFilter
|
|
7231
8178
|
});
|
|
7232
8179
|
if (embeddingRows.length === 0) return [];
|
|
8180
|
+
const embeddingMediaKeyByTrackId = /* @__PURE__ */ new Map();
|
|
8181
|
+
const keyFrameKeyByTrackId = /* @__PURE__ */ new Map();
|
|
8182
|
+
for (const row of embeddingRows) {
|
|
8183
|
+
if (row.mediaKey !== void 0) embeddingMediaKeyByTrackId.set(row.trackId, row.mediaKey);
|
|
8184
|
+
if (row.keyFrameMediaKey !== void 0) keyFrameKeyByTrackId.set(row.trackId, row.keyFrameMediaKey);
|
|
8185
|
+
}
|
|
7233
8186
|
const encoderInfo = await api.embeddingEncoder.getInfo.query().catch((err) => {
|
|
7234
8187
|
this.ctx.logger.warn("searchObjectEvents: getInfo failed, proceeding without modelId gate", { meta: { error: String(err) } });
|
|
7235
8188
|
return null;
|
|
@@ -7276,10 +8229,20 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
7276
8229
|
score
|
|
7277
8230
|
});
|
|
7278
8231
|
}
|
|
7279
|
-
for (const { event, score } of bestEventByTrackId.values())
|
|
7280
|
-
|
|
7281
|
-
|
|
7282
|
-
|
|
8232
|
+
for (const { event, score } of bestEventByTrackId.values()) {
|
|
8233
|
+
const mediaUrl = resolveSearchThumbnailUrl({
|
|
8234
|
+
baseUrl: this.eventMediaBaseUrl,
|
|
8235
|
+
eventId: event.id,
|
|
8236
|
+
...event.trackId !== void 0 && embeddingMediaKeyByTrackId.has(event.trackId) ? { embeddingMediaKey: embeddingMediaKeyByTrackId.get(event.trackId) } : {}
|
|
8237
|
+
});
|
|
8238
|
+
const keyFrameMediaKey = event.trackId !== void 0 ? keyFrameKeyByTrackId.get(event.trackId) : void 0;
|
|
8239
|
+
scored.push({
|
|
8240
|
+
...event,
|
|
8241
|
+
score,
|
|
8242
|
+
...mediaUrl !== void 0 ? { mediaUrl } : {},
|
|
8243
|
+
...keyFrameMediaKey !== void 0 ? { keyFrameMediaKey } : {}
|
|
8244
|
+
});
|
|
8245
|
+
}
|
|
7283
8246
|
}
|
|
7284
8247
|
scored.sort((a, b) => b.score - a.score);
|
|
7285
8248
|
return scored.slice(0, input.limit);
|
|
@@ -7311,6 +8274,24 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
7311
8274
|
* (native-res boxed frame) → any available file. Returns `null` if the
|
|
7312
8275
|
* event has no media at all.
|
|
7313
8276
|
*/
|
|
8277
|
+
/**
|
|
8278
|
+
* Data-plane resolver: an id is EITHER a bare event id (a UUID → the event's
|
|
8279
|
+
* crop, today's behaviour) OR a MediaStore key (`ownerKind:ownerId:kind:ts`,
|
|
8280
|
+
* contains ':' → served directly by key). The object-embedding search hit
|
|
8281
|
+
* points its thumbnail at the embedding row's crop key (a `track:…:crop:…`
|
|
8282
|
+
* key), so this resolves that crop; event ids stay on the event-crop path.
|
|
8283
|
+
*/
|
|
8284
|
+
async readMediaByEventOrKey(id) {
|
|
8285
|
+
if (id.includes(":")) {
|
|
8286
|
+
const file = await (this.mediaStore?.getByKey(id) ?? Promise.resolve(null));
|
|
8287
|
+
if (!file) return null;
|
|
8288
|
+
return {
|
|
8289
|
+
bytes: Buffer.from(file.base64, "base64"),
|
|
8290
|
+
key: file.key
|
|
8291
|
+
};
|
|
8292
|
+
}
|
|
8293
|
+
return this.readEventThumbnail(id);
|
|
8294
|
+
}
|
|
7314
8295
|
async readEventThumbnail(eventId) {
|
|
7315
8296
|
const files = await (this.mediaStore?.listByOwner("event", eventId) ?? Promise.resolve([]));
|
|
7316
8297
|
const chosen = files.find((f) => f.kind === "crop") ?? files.find((f) => f.kind === "fullFrameBoxed") ?? files[0];
|
|
@@ -7399,7 +8380,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
7399
8380
|
min: 500,
|
|
7400
8381
|
max: 6e4,
|
|
7401
8382
|
step: 500,
|
|
7402
|
-
default:
|
|
8383
|
+
default: 5e3,
|
|
7403
8384
|
showValue: true,
|
|
7404
8385
|
unit: "s",
|
|
7405
8386
|
displayScale: 1e3
|