@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as
|
|
1
|
+
import { C as number, S as boolean, T as string, c as nodePin, d as videoclipsCapability, f as zoneAnalyticsCapability, g as EventCategory, h as DeviceType, i as cosineSimilarity, l as pipelineAnalyticsCapability, m as BaseAddon, n as addonWidgetsSourceCapability, o as faceGalleryCapability, p as errMsg, r as audioMetricsCapability, t as EVENT_PAD_MS, u as plateGalleryCapability, w as object, y as hydrateSchema } from "../dist-DytVmDZg.mjs";
|
|
2
2
|
import { n as extractCrop, t as resolveFrame } from "../resolve-frame-CT1T1tWy.mjs";
|
|
3
3
|
import { FrameRingReaderCache } from "@camstack/shm-ring";
|
|
4
4
|
import sharp from "sharp";
|
|
@@ -119,6 +119,138 @@ function clusterByEmbedding(items, threshold = .5) {
|
|
|
119
119
|
})).toSorted((a, b) => b.size - a.size || b.cohesion - a.cohesion);
|
|
120
120
|
}
|
|
121
121
|
//#endregion
|
|
122
|
+
//#region src/pipeline-analytics/pipeline/key-event-scoring.ts
|
|
123
|
+
/**
|
|
124
|
+
* key-event-scoring — the SINGLE deterministic "importance" scorer for a
|
|
125
|
+
* completed track. A value in [0,1] the viewer sorts events by (events page)
|
|
126
|
+
* and a future reels feature pulls highlights from.
|
|
127
|
+
*
|
|
128
|
+
* PURE + DETERMINISTIC: no Date.now / no random. Time enters ONLY as the
|
|
129
|
+
* precomputed `durationMs` (the caller already knows a track's dwell). Given
|
|
130
|
+
* the same `ScoringInput`, `computeImportance` always returns the same
|
|
131
|
+
* `{ importance, reason }`.
|
|
132
|
+
*
|
|
133
|
+
* The score is a weighted sum of seven independent, individually-saturated
|
|
134
|
+
* signals. Every weight and knee is a NAMED EXPORT so the formula is auditable
|
|
135
|
+
* and the tests can assert exact wiring. The weights sum to exactly 1.0, so an
|
|
136
|
+
* all-signals-maxed track scores exactly 1.0 (the clamp is then a no-op) and an
|
|
137
|
+
* all-zero track scores 0.
|
|
138
|
+
*/
|
|
139
|
+
var WEIGHT_CONFIDENCE = .3;
|
|
140
|
+
var WEIGHT_CLASS = .2;
|
|
141
|
+
var WEIGHT_DWELL = .2;
|
|
142
|
+
var WEIGHT_PROXIMITY = .12;
|
|
143
|
+
var WEIGHT_TRAVEL = .08;
|
|
144
|
+
var WEIGHT_ZONE = .05;
|
|
145
|
+
var WEIGHT_IDENTITY = .05;
|
|
146
|
+
/** Dwell (ms) that saturates the dwell term. */
|
|
147
|
+
var DWELL_FULL_MS = 6e4;
|
|
148
|
+
/** Peak bbox area (as a fraction of frame area) that saturates the size term. */
|
|
149
|
+
var SIZE_FULL = .15;
|
|
150
|
+
var CLASS_RANK_VEHICLE = .8;
|
|
151
|
+
var CLASS_RANK_ANIMAL = .5;
|
|
152
|
+
var CLASS_RANK_DEFAULT = .25;
|
|
153
|
+
var PERSON_CLASSES = new Set(["person", "face"]);
|
|
154
|
+
var VEHICLE_CLASSES = new Set([
|
|
155
|
+
"vehicle",
|
|
156
|
+
"car",
|
|
157
|
+
"truck",
|
|
158
|
+
"bus"
|
|
159
|
+
]);
|
|
160
|
+
var ANIMAL_CLASSES = new Set([
|
|
161
|
+
"animal",
|
|
162
|
+
"dog",
|
|
163
|
+
"cat"
|
|
164
|
+
]);
|
|
165
|
+
/** Clamp to [0,1]. */
|
|
166
|
+
function clamp01(x) {
|
|
167
|
+
if (Number.isNaN(x)) return 0;
|
|
168
|
+
if (x < 0) return 0;
|
|
169
|
+
if (x > 1) return 1;
|
|
170
|
+
return x;
|
|
171
|
+
}
|
|
172
|
+
/** Rank a class name into [0,1] by how event-worthy it is. Case-insensitive. */
|
|
173
|
+
function classRank(className) {
|
|
174
|
+
const c = className.toLowerCase();
|
|
175
|
+
if (PERSON_CLASSES.has(c)) return 1;
|
|
176
|
+
if (VEHICLE_CLASSES.has(c)) return CLASS_RANK_VEHICLE;
|
|
177
|
+
if (ANIMAL_CLASSES.has(c)) return CLASS_RANK_ANIMAL;
|
|
178
|
+
return CLASS_RANK_DEFAULT;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Compute a deterministic importance score for a completed track.
|
|
182
|
+
*
|
|
183
|
+
* `reason` is the tag of the single largest weighted term. Ties resolve to the
|
|
184
|
+
* first term in a fixed, weight-descending order (confidence → class → dwell →
|
|
185
|
+
* proximity → travel → zone → identity), so the output stays deterministic.
|
|
186
|
+
*/
|
|
187
|
+
function computeImportance(input) {
|
|
188
|
+
const terms = [
|
|
189
|
+
{
|
|
190
|
+
reason: "confidence",
|
|
191
|
+
value: WEIGHT_CONFIDENCE * clamp01(input.peakConfidence)
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
reason: "class",
|
|
195
|
+
value: WEIGHT_CLASS * classRank(input.className)
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
reason: "dwell",
|
|
199
|
+
value: WEIGHT_DWELL * clamp01(input.durationMs / DWELL_FULL_MS)
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
reason: "proximity",
|
|
203
|
+
value: WEIGHT_PROXIMITY * clamp01(input.peakBboxAreaFrac / SIZE_FULL)
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
reason: "travel",
|
|
207
|
+
value: WEIGHT_TRAVEL * clamp01(input.totalDistance / 2)
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
reason: "zone",
|
|
211
|
+
value: WEIGHT_ZONE * (input.zonesVisited.length > 0 ? 1 : 0)
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
reason: "identity",
|
|
215
|
+
value: WEIGHT_IDENTITY * (input.label ? 1 : 0)
|
|
216
|
+
}
|
|
217
|
+
];
|
|
218
|
+
let sum = 0;
|
|
219
|
+
let best = terms[0];
|
|
220
|
+
for (const term of terms) {
|
|
221
|
+
sum += term.value;
|
|
222
|
+
if (term.value > best.value) best = term;
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
importance: clamp01(sum),
|
|
226
|
+
reason: best.reason
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region src/pipeline-analytics/pipeline/key-event-recompute.ts
|
|
231
|
+
/**
|
|
232
|
+
* Recompute + persist a persisted track's importance from its current fields
|
|
233
|
+
* (including any newly-assigned label) and re-stamp its object events. No-op
|
|
234
|
+
* when the track is not (yet) persisted.
|
|
235
|
+
*/
|
|
236
|
+
async function recomputeTrackImportance(deps, trackId) {
|
|
237
|
+
const track = await deps.trackStore.getPersistedByTrackId(trackId);
|
|
238
|
+
if (track === null) return;
|
|
239
|
+
const peak = await deps.eventStore.peakForTrack(trackId);
|
|
240
|
+
const { importance, reason } = computeImportance({
|
|
241
|
+
peakConfidence: peak.peakConfidence,
|
|
242
|
+
className: track.className,
|
|
243
|
+
durationMs: track.lastSeen - track.firstSeen,
|
|
244
|
+
peakBboxAreaFrac: peak.peakBboxAreaFrac,
|
|
245
|
+
totalDistance: track.totalDistance,
|
|
246
|
+
zonesVisited: track.zonesVisited,
|
|
247
|
+
...track.label !== void 0 ? { label: track.label } : {}
|
|
248
|
+
});
|
|
249
|
+
const bestEventId = track.bestEventId ?? peak.bestEventId;
|
|
250
|
+
await deps.trackStore.setImportance(trackId, importance, reason, bestEventId);
|
|
251
|
+
await deps.eventStore.setImportanceForTrack(trackId, importance);
|
|
252
|
+
}
|
|
253
|
+
//#endregion
|
|
122
254
|
//#region src/pipeline-analytics/face-gallery-provider.ts
|
|
123
255
|
/** Embedding model id used when enrolling face crops as identity samples. */
|
|
124
256
|
var MODEL_ID = "arcface-r100";
|
|
@@ -239,7 +371,9 @@ var FaceGalleryProvider = class {
|
|
|
239
371
|
...face.recognizedIdentityId != null ? { recognizedIdentityId: face.recognizedIdentityId } : {},
|
|
240
372
|
...identityName !== void 0 ? { identityName } : {},
|
|
241
373
|
assigned: face.assigned,
|
|
242
|
-
...base64 !== void 0 ? { base64 } : {}
|
|
374
|
+
...base64 !== void 0 ? { base64 } : {},
|
|
375
|
+
...face.faceBbox !== void 0 ? { faceBbox: face.faceBbox } : {},
|
|
376
|
+
...face.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: face.keyFrameMediaKey } : {}
|
|
243
377
|
});
|
|
244
378
|
}
|
|
245
379
|
return result;
|
|
@@ -267,7 +401,9 @@ var FaceGalleryProvider = class {
|
|
|
267
401
|
...face.recognizedIdentityId != null ? { recognizedIdentityId: face.recognizedIdentityId } : {},
|
|
268
402
|
...identityName !== void 0 ? { identityName } : {},
|
|
269
403
|
assigned: face.assigned,
|
|
270
|
-
...base64 !== void 0 ? { base64 } : {}
|
|
404
|
+
...base64 !== void 0 ? { base64 } : {},
|
|
405
|
+
...face.faceBbox !== void 0 ? { faceBbox: face.faceBbox } : {},
|
|
406
|
+
...face.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: face.keyFrameMediaKey } : {}
|
|
271
407
|
};
|
|
272
408
|
}
|
|
273
409
|
/**
|
|
@@ -344,6 +480,10 @@ var FaceGalleryProvider = class {
|
|
|
344
480
|
if (identityName !== void 0) {
|
|
345
481
|
await this.trackStore.setLabel(currentFace.trackId, identityName);
|
|
346
482
|
await this.eventStore.setLabelForTrack(currentFace.trackId, identityName);
|
|
483
|
+
await recomputeTrackImportance({
|
|
484
|
+
trackStore: this.trackStore,
|
|
485
|
+
eventStore: this.eventStore
|
|
486
|
+
}, currentFace.trackId);
|
|
347
487
|
}
|
|
348
488
|
this.refreshGallery();
|
|
349
489
|
}
|
|
@@ -1424,6 +1564,10 @@ var FrameProcessor = class {
|
|
|
1424
1564
|
w: det.bbox.width,
|
|
1425
1565
|
h: det.bbox.height
|
|
1426
1566
|
});
|
|
1567
|
+
if (det.embedding !== void 0 && !embeddingByBbox.has(parentBbox)) embeddingByBbox.set(parentBbox, {
|
|
1568
|
+
embedding: det.embedding,
|
|
1569
|
+
...det.embeddingModelId !== void 0 ? { embeddingModelId: det.embeddingModelId } : {}
|
|
1570
|
+
});
|
|
1427
1571
|
}
|
|
1428
1572
|
for (const det of frame.detections) {
|
|
1429
1573
|
if (det.kind !== "detail" || det.macroClass !== "plate" || !det.parentId) continue;
|
|
@@ -1527,6 +1671,152 @@ var FrameProcessor = class {
|
|
|
1527
1671
|
}
|
|
1528
1672
|
};
|
|
1529
1673
|
//#endregion
|
|
1674
|
+
//#region src/pipeline-analytics/pipeline/best-detection-tracker.ts
|
|
1675
|
+
var BestDetectionTracker = class {
|
|
1676
|
+
hysteresis;
|
|
1677
|
+
minGapMs;
|
|
1678
|
+
best = /* @__PURE__ */ new Map();
|
|
1679
|
+
constructor(options = {}) {
|
|
1680
|
+
this.hysteresis = options.hysteresis ?? 0;
|
|
1681
|
+
this.minGapMs = options.minGapMs ?? 0;
|
|
1682
|
+
}
|
|
1683
|
+
/**
|
|
1684
|
+
* Record a detection's `confidence` (at wall-clock `timestamp`) for `trackId`.
|
|
1685
|
+
* Returns true when it becomes the track's new best — the first sighting, or a
|
|
1686
|
+
* confidence that beats the held peak by more than `hysteresis` AND respects
|
|
1687
|
+
* `minGapMs`. On acceptance the held peak is advanced to this observation.
|
|
1688
|
+
*/
|
|
1689
|
+
observe(trackId, confidence, timestamp) {
|
|
1690
|
+
const cur = this.best.get(trackId);
|
|
1691
|
+
const isNewBest = cur === void 0 || confidence > cur.confidence + this.hysteresis && timestamp - cur.atMs >= this.minGapMs;
|
|
1692
|
+
if (isNewBest) this.best.set(trackId, {
|
|
1693
|
+
confidence,
|
|
1694
|
+
atMs: timestamp
|
|
1695
|
+
});
|
|
1696
|
+
return isNewBest;
|
|
1697
|
+
}
|
|
1698
|
+
/** The held peak for a track (undefined if never observed). */
|
|
1699
|
+
peak(trackId) {
|
|
1700
|
+
return this.best.get(trackId);
|
|
1701
|
+
}
|
|
1702
|
+
/** Drop a track's peak (call at track end). */
|
|
1703
|
+
delete(trackId) {
|
|
1704
|
+
this.best.delete(trackId);
|
|
1705
|
+
}
|
|
1706
|
+
clear() {
|
|
1707
|
+
this.best.clear();
|
|
1708
|
+
}
|
|
1709
|
+
};
|
|
1710
|
+
//#endregion
|
|
1711
|
+
//#region src/pipeline-analytics/pipeline/track-best-detection.ts
|
|
1712
|
+
/**
|
|
1713
|
+
* `TrackBestSelector` — the ONE unified "best detection per track" primitive.
|
|
1714
|
+
*
|
|
1715
|
+
* Post-analysis derives several per-track "best" artefacts (best FRAME thumbnail,
|
|
1716
|
+
* best OBJECT/CLIP embedding + its crop, best FACE crop). They all rank the same
|
|
1717
|
+
* way: highest detector confidence per `trackId`. This selector composes the
|
|
1718
|
+
* canonical {@link BestDetectionTracker} ranking with the RESOLVED payload the
|
|
1719
|
+
* consumers need at the peak (bbox / className / embedding / faceBbox), so a
|
|
1720
|
+
* single "new best" DECISION can drive one capture whose output is shared:
|
|
1721
|
+
* - the boxed best-frame `thumbnail`,
|
|
1722
|
+
* - the tight object crop written onto the CLIP embedding row's `mediaKey`
|
|
1723
|
+
* (so a search hit's thumbnail IS the embedded crop).
|
|
1724
|
+
*
|
|
1725
|
+
* It is deliberately in-memory (the peak is per live track, dropped at track
|
|
1726
|
+
* end). The CLIP embedding's cross-restart persistence stays a SEPARATE store-
|
|
1727
|
+
* side gate (`ObjectEmbeddingStore.upsertIfBetter`): this selector unifies the
|
|
1728
|
+
* DECISION, not the durable store (see best-detection-tracker.ts docstring).
|
|
1729
|
+
*
|
|
1730
|
+
* The face path keeps its own hold buffer because a face crop can only come
|
|
1731
|
+
* from a face-bearing frame (the documented FRAME↔FACE seam) — but it shares
|
|
1732
|
+
* this ranking so best-frame and best-face agree on which frame is "best".
|
|
1733
|
+
*/
|
|
1734
|
+
var TrackBestSelector = class {
|
|
1735
|
+
tracker;
|
|
1736
|
+
payloads = /* @__PURE__ */ new Map();
|
|
1737
|
+
constructor(options = {}) {
|
|
1738
|
+
this.tracker = new BestDetectionTracker(options);
|
|
1739
|
+
}
|
|
1740
|
+
/**
|
|
1741
|
+
* Record an observation for its track. Returns true when it becomes the
|
|
1742
|
+
* track's new best (first sighting, or a confidence that beats the held peak
|
|
1743
|
+
* per the tracker's hysteresis / minGap rules). On acceptance the held payload
|
|
1744
|
+
* advances to this observation so `peak(trackId)` returns the winning frame's
|
|
1745
|
+
* bbox / embedding / faceBbox.
|
|
1746
|
+
*/
|
|
1747
|
+
observe(obs) {
|
|
1748
|
+
const isNewBest = this.tracker.observe(obs.trackId, obs.confidence, obs.atMs);
|
|
1749
|
+
if (isNewBest) {
|
|
1750
|
+
const { trackId, ...payload } = obs;
|
|
1751
|
+
this.payloads.set(trackId, payload);
|
|
1752
|
+
}
|
|
1753
|
+
return isNewBest;
|
|
1754
|
+
}
|
|
1755
|
+
/** The held best payload for a track (undefined if never observed). */
|
|
1756
|
+
peak(trackId) {
|
|
1757
|
+
return this.payloads.get(trackId);
|
|
1758
|
+
}
|
|
1759
|
+
/** The held peak confidence for a track (undefined if never observed). */
|
|
1760
|
+
peakConfidence(trackId) {
|
|
1761
|
+
return this.tracker.peak(trackId)?.confidence;
|
|
1762
|
+
}
|
|
1763
|
+
/** Drop a track's peak + payload (call at track end). */
|
|
1764
|
+
delete(trackId) {
|
|
1765
|
+
this.tracker.delete(trackId);
|
|
1766
|
+
this.payloads.delete(trackId);
|
|
1767
|
+
}
|
|
1768
|
+
clear() {
|
|
1769
|
+
this.tracker.clear();
|
|
1770
|
+
this.payloads.clear();
|
|
1771
|
+
}
|
|
1772
|
+
};
|
|
1773
|
+
//#endregion
|
|
1774
|
+
//#region src/pipeline-analytics/pipeline/object-embedding-selection.ts
|
|
1775
|
+
function isClipObjectEmbedding(t) {
|
|
1776
|
+
return Array.isArray(t.embedding) && t.embedding.length > 0 && typeof t.embeddingModelId === "string" && t.embeddingModelId.startsWith("mobileclip-");
|
|
1777
|
+
}
|
|
1778
|
+
function resolveSearchThumbnailUrl(input) {
|
|
1779
|
+
if (input.baseUrl === null) return void 0;
|
|
1780
|
+
const id = input.embeddingMediaKey ?? input.eventId;
|
|
1781
|
+
return `${input.baseUrl}/${encodeURIComponent(id)}`;
|
|
1782
|
+
}
|
|
1783
|
+
//#endregion
|
|
1784
|
+
//#region src/pipeline-analytics/pipeline/key-event-query.ts
|
|
1785
|
+
async function rankKeyEvents(candidates, options, peakLookup) {
|
|
1786
|
+
const scored = [];
|
|
1787
|
+
for (const t of candidates) {
|
|
1788
|
+
if (options.classFilter !== void 0 && t.className !== options.classFilter) continue;
|
|
1789
|
+
let importance = t.importance;
|
|
1790
|
+
let bestEventId = t.bestEventId;
|
|
1791
|
+
if (importance === void 0) {
|
|
1792
|
+
const peak = await peakLookup(t.trackId);
|
|
1793
|
+
importance = computeImportance({
|
|
1794
|
+
peakConfidence: peak.peakConfidence,
|
|
1795
|
+
className: t.className,
|
|
1796
|
+
durationMs: t.lastSeen - t.firstSeen,
|
|
1797
|
+
peakBboxAreaFrac: peak.peakBboxAreaFrac,
|
|
1798
|
+
totalDistance: t.totalDistance,
|
|
1799
|
+
zonesVisited: t.zonesVisited,
|
|
1800
|
+
...t.label !== void 0 ? { label: t.label } : {}
|
|
1801
|
+
}).importance;
|
|
1802
|
+
bestEventId = bestEventId ?? peak.bestEventId;
|
|
1803
|
+
}
|
|
1804
|
+
if (options.minImportance !== void 0 && importance < options.minImportance) continue;
|
|
1805
|
+
scored.push({
|
|
1806
|
+
id: bestEventId ?? t.trackId,
|
|
1807
|
+
trackId: t.trackId,
|
|
1808
|
+
timestamp: t.firstSeen,
|
|
1809
|
+
className: t.className,
|
|
1810
|
+
...t.label !== void 0 ? { label: t.label } : {},
|
|
1811
|
+
importance,
|
|
1812
|
+
bestEventId: bestEventId ?? "",
|
|
1813
|
+
windowMs: t.lastSeen - t.firstSeen
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
scored.sort((a, b) => b.importance - a.importance);
|
|
1817
|
+
return scored.slice(0, options.limit);
|
|
1818
|
+
}
|
|
1819
|
+
//#endregion
|
|
1530
1820
|
//#region src/pipeline-analytics/pipeline/native-detection.ts
|
|
1531
1821
|
/**
|
|
1532
1822
|
* Nominal frame size used to denormalize native `[0,1]` boxes when a
|
|
@@ -1696,6 +1986,18 @@ var TRACKS_COLUMNS = [
|
|
|
1696
1986
|
{
|
|
1697
1987
|
name: "state",
|
|
1698
1988
|
type: "TEXT"
|
|
1989
|
+
},
|
|
1990
|
+
{
|
|
1991
|
+
name: "importance",
|
|
1992
|
+
type: "REAL"
|
|
1993
|
+
},
|
|
1994
|
+
{
|
|
1995
|
+
name: "bestEventId",
|
|
1996
|
+
type: "TEXT"
|
|
1997
|
+
},
|
|
1998
|
+
{
|
|
1999
|
+
name: "importanceReason",
|
|
2000
|
+
type: "TEXT"
|
|
1699
2001
|
}
|
|
1700
2002
|
];
|
|
1701
2003
|
var TRACKS_INDEXES = [{
|
|
@@ -1727,7 +2029,10 @@ function cloneTrack(t) {
|
|
|
1727
2029
|
zonesVisited: [...t.zonesVisited],
|
|
1728
2030
|
totalDistance: t.totalDistance,
|
|
1729
2031
|
state: t.state,
|
|
1730
|
-
active: t.active
|
|
2032
|
+
active: t.active,
|
|
2033
|
+
...t.importance !== void 0 ? { importance: t.importance } : {},
|
|
2034
|
+
...t.bestEventId !== void 0 ? { bestEventId: t.bestEventId } : {},
|
|
2035
|
+
...t.importanceReason !== void 0 ? { importanceReason: t.importanceReason } : {}
|
|
1731
2036
|
};
|
|
1732
2037
|
}
|
|
1733
2038
|
var TrackStore = class {
|
|
@@ -1796,6 +2101,16 @@ var TrackStore = class {
|
|
|
1796
2101
|
lastSnapshotAt(trackId) {
|
|
1797
2102
|
return this.active.get(trackId)?.lastSnapshotAt ?? 0;
|
|
1798
2103
|
}
|
|
2104
|
+
/**
|
|
2105
|
+
* Seed the snapshot cadence clock (once, at track start) WITHOUT appending a
|
|
2106
|
+
* snapshot, so the first periodic snapshot fires ~snapshotIntervalMs after the
|
|
2107
|
+
* track begins rather than immediately — the `firstFrame` already covers the
|
|
2108
|
+
* track's start. No-op if a snapshot was already taken (clock already set).
|
|
2109
|
+
*/
|
|
2110
|
+
seedSnapshotClock(trackId, timestamp) {
|
|
2111
|
+
const t = this.active.get(trackId);
|
|
2112
|
+
if (t && t.lastSnapshotAt === 0) t.lastSnapshotAt = timestamp;
|
|
2113
|
+
}
|
|
1799
2114
|
getActive(deviceId) {
|
|
1800
2115
|
const out = [];
|
|
1801
2116
|
for (const t of this.active.values()) if (t.deviceId === deviceId && t.active) out.push(cloneTrack(t));
|
|
@@ -1851,6 +2166,37 @@ var TrackStore = class {
|
|
|
1851
2166
|
}
|
|
1852
2167
|
}
|
|
1853
2168
|
/**
|
|
2169
|
+
* Stamp a track's importance score (+ dominant reason and best-event pointer).
|
|
2170
|
+
* Updates the in-memory active entry (so the value is carried into an eventual
|
|
2171
|
+
* (re)persist) AND patches the already-persisted row. Mirrors `setLabel`.
|
|
2172
|
+
* Forward-only — never rewrites history beyond these fields.
|
|
2173
|
+
*/
|
|
2174
|
+
async setImportance(trackId, importance, reason, bestEventId) {
|
|
2175
|
+
const active = this.active.get(trackId);
|
|
2176
|
+
if (active) {
|
|
2177
|
+
active.importance = importance;
|
|
2178
|
+
active.importanceReason = reason;
|
|
2179
|
+
if (bestEventId !== void 0) active.bestEventId = bestEventId;
|
|
2180
|
+
}
|
|
2181
|
+
const data = {
|
|
2182
|
+
importance,
|
|
2183
|
+
importanceReason: reason
|
|
2184
|
+
};
|
|
2185
|
+
if (bestEventId !== void 0) data["bestEventId"] = bestEventId;
|
|
2186
|
+
try {
|
|
2187
|
+
await this.store.update.mutate({
|
|
2188
|
+
collection: TRACKS_COLLECTION,
|
|
2189
|
+
id: trackId,
|
|
2190
|
+
data
|
|
2191
|
+
});
|
|
2192
|
+
} catch (err) {
|
|
2193
|
+
this.logger.warn("setImportance persist failed", { meta: {
|
|
2194
|
+
trackId,
|
|
2195
|
+
error: String(err)
|
|
2196
|
+
} });
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
/**
|
|
1854
2200
|
* Clear a track's label. Sets label to null in the persisted row (so
|
|
1855
2201
|
* rowToTrack's `typeof label === 'string'` guard omits it on read → label
|
|
1856
2202
|
* is absent/undefined). Also clears the in-memory active entry if present.
|
|
@@ -1921,7 +2267,10 @@ var TrackStore = class {
|
|
|
1921
2267
|
snapshots: [...t.snapshots],
|
|
1922
2268
|
zonesVisited: [...t.zonesVisited],
|
|
1923
2269
|
totalDistance: t.totalDistance,
|
|
1924
|
-
state: t.state
|
|
2270
|
+
state: t.state,
|
|
2271
|
+
...t.importance !== void 0 ? { importance: t.importance } : {},
|
|
2272
|
+
...t.bestEventId !== void 0 ? { bestEventId: t.bestEventId } : {},
|
|
2273
|
+
...t.importanceReason !== void 0 ? { importanceReason: t.importanceReason } : {}
|
|
1925
2274
|
}
|
|
1926
2275
|
});
|
|
1927
2276
|
}
|
|
@@ -1930,6 +2279,9 @@ var TrackStore = class {
|
|
|
1930
2279
|
const snapshots = data["snapshots"] ?? [];
|
|
1931
2280
|
const zones = data["zonesVisited"] ?? [];
|
|
1932
2281
|
const label = data["label"];
|
|
2282
|
+
const importance = data["importance"];
|
|
2283
|
+
const bestEventId = data["bestEventId"];
|
|
2284
|
+
const importanceReason = data["importanceReason"];
|
|
1933
2285
|
return {
|
|
1934
2286
|
trackId: id,
|
|
1935
2287
|
deviceId: Number(data["deviceId"]),
|
|
@@ -1942,7 +2294,10 @@ var TrackStore = class {
|
|
|
1942
2294
|
zonesVisited: zones,
|
|
1943
2295
|
totalDistance: Number(data["totalDistance"] ?? 0),
|
|
1944
2296
|
state: data["state"] ?? "idle",
|
|
1945
|
-
active: false
|
|
2297
|
+
active: false,
|
|
2298
|
+
...typeof importance === "number" ? { importance } : {},
|
|
2299
|
+
...typeof bestEventId === "string" ? { bestEventId } : {},
|
|
2300
|
+
...typeof importanceReason === "string" ? { importanceReason } : {}
|
|
1946
2301
|
};
|
|
1947
2302
|
}
|
|
1948
2303
|
};
|
|
@@ -2060,6 +2415,46 @@ var MediaStore = class {
|
|
|
2060
2415
|
}
|
|
2061
2416
|
}
|
|
2062
2417
|
/**
|
|
2418
|
+
* Overwrite semantics: keep exactly ONE media entry per (ownerKind, ownerId,
|
|
2419
|
+
* kind). Used for the rolling `lastFrame` and the best `thumbnail` per track —
|
|
2420
|
+
* each new capture replaces the previous one (blob + index row) rather than
|
|
2421
|
+
* accumulating a filmstrip the way `put` does. Deletes any existing rows of
|
|
2422
|
+
* that (owner, kind) first, then writes the fresh one. Returns the new key.
|
|
2423
|
+
*/
|
|
2424
|
+
async putReplacing(params) {
|
|
2425
|
+
const existing = await this.store.query.query({
|
|
2426
|
+
collection: MEDIA_COLLECTION,
|
|
2427
|
+
filter: { where: {
|
|
2428
|
+
ownerKind: params.ownerKind,
|
|
2429
|
+
ownerId: params.ownerId,
|
|
2430
|
+
kind: params.kind
|
|
2431
|
+
} }
|
|
2432
|
+
});
|
|
2433
|
+
const newKey = await this.put(params);
|
|
2434
|
+
for (const row of existing) {
|
|
2435
|
+
if (row.id === newKey) continue;
|
|
2436
|
+
const path = String(row.data["path"] ?? "");
|
|
2437
|
+
if (path) try {
|
|
2438
|
+
await this.storage.delete({
|
|
2439
|
+
location: "eventMedia",
|
|
2440
|
+
relativePath: path
|
|
2441
|
+
});
|
|
2442
|
+
} catch {}
|
|
2443
|
+
try {
|
|
2444
|
+
await this.store.delete.mutate({
|
|
2445
|
+
collection: MEDIA_COLLECTION,
|
|
2446
|
+
key: row.id
|
|
2447
|
+
});
|
|
2448
|
+
} catch (err) {
|
|
2449
|
+
this.logger.debug("media putReplacing: stale row delete failed", { meta: {
|
|
2450
|
+
key: row.id,
|
|
2451
|
+
error: String(err)
|
|
2452
|
+
} });
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
return newKey;
|
|
2456
|
+
}
|
|
2457
|
+
/**
|
|
2063
2458
|
* Fetch one media entry by its key (id). Returns null if the key is not
|
|
2064
2459
|
* found in the index or if the blob is missing from storage.
|
|
2065
2460
|
*/
|
|
@@ -2405,6 +2800,10 @@ var OBJECT_COLUMNS = [
|
|
|
2405
2800
|
{
|
|
2406
2801
|
name: "mediaKey",
|
|
2407
2802
|
type: "TEXT"
|
|
2803
|
+
},
|
|
2804
|
+
{
|
|
2805
|
+
name: "importance",
|
|
2806
|
+
type: "REAL"
|
|
2408
2807
|
}
|
|
2409
2808
|
];
|
|
2410
2809
|
var AUDIO_COLUMNS = [
|
|
@@ -2602,6 +3001,63 @@ var EventStore = class {
|
|
|
2602
3001
|
return updated;
|
|
2603
3002
|
}
|
|
2604
3003
|
/**
|
|
3004
|
+
* Forward-only: stamp `importance` on every already-emitted object event of a
|
|
3005
|
+
* track. Returns the number of events updated. Best-effort per row. Mirrors
|
|
3006
|
+
* `setLabelForTrack`. Called when a track's key-event score is computed (at
|
|
3007
|
+
* expiry) or recomputed (late label) so an event row carries the parent
|
|
3008
|
+
* track's importance without a join.
|
|
3009
|
+
*/
|
|
3010
|
+
async setImportanceForTrack(trackId, importance) {
|
|
3011
|
+
const rows = await this.store.query.query({
|
|
3012
|
+
collection: OBJECT_EVENTS_COLLECTION,
|
|
3013
|
+
filter: { where: { trackId } }
|
|
3014
|
+
});
|
|
3015
|
+
let updated = 0;
|
|
3016
|
+
for (const row of rows) try {
|
|
3017
|
+
await this.store.update.mutate({
|
|
3018
|
+
collection: OBJECT_EVENTS_COLLECTION,
|
|
3019
|
+
id: row.id,
|
|
3020
|
+
data: { importance }
|
|
3021
|
+
});
|
|
3022
|
+
updated++;
|
|
3023
|
+
} catch (err) {
|
|
3024
|
+
this.logger.warn("setImportanceForTrack update failed", { meta: {
|
|
3025
|
+
trackId,
|
|
3026
|
+
eventId: row.id,
|
|
3027
|
+
error: String(err)
|
|
3028
|
+
} });
|
|
3029
|
+
}
|
|
3030
|
+
return updated;
|
|
3031
|
+
}
|
|
3032
|
+
/**
|
|
3033
|
+
* The track's highest-confidence object event, its bbox area (as a fraction of
|
|
3034
|
+
* frame area), and that event's id — the SHARED per-track ranking already used
|
|
3035
|
+
* for the best frame, read back from the persisted object events (index
|
|
3036
|
+
* `idx_object_track`). Returns zeros + undefined id when the track has none.
|
|
3037
|
+
* Used by the importance scorer at expiry and by `getKeyEvents` compute-on-read.
|
|
3038
|
+
*/
|
|
3039
|
+
async peakForTrack(trackId) {
|
|
3040
|
+
const rows = await this.store.query.query({
|
|
3041
|
+
collection: OBJECT_EVENTS_COLLECTION,
|
|
3042
|
+
filter: { where: { trackId } }
|
|
3043
|
+
});
|
|
3044
|
+
let bestConf = -1;
|
|
3045
|
+
let bestEventId;
|
|
3046
|
+
let peakBboxAreaFrac = 0;
|
|
3047
|
+
for (const row of rows) {
|
|
3048
|
+
const conf = typeof row.data["confidence"] === "number" ? row.data["confidence"] : 0;
|
|
3049
|
+
if (conf <= bestConf) continue;
|
|
3050
|
+
bestConf = conf;
|
|
3051
|
+
bestEventId = row.id;
|
|
3052
|
+
peakBboxAreaFrac = bboxAreaFrac(row.data);
|
|
3053
|
+
}
|
|
3054
|
+
return {
|
|
3055
|
+
peakConfidence: bestConf < 0 ? 0 : bestConf,
|
|
3056
|
+
peakBboxAreaFrac,
|
|
3057
|
+
bestEventId
|
|
3058
|
+
};
|
|
3059
|
+
}
|
|
3060
|
+
/**
|
|
2605
3061
|
* Clear `label` on every already-emitted object event of a track (sets label
|
|
2606
3062
|
* to null so stripNulls/slimObject omit it on read → label is absent/undefined).
|
|
2607
3063
|
* Returns the number of events updated. Best-effort per row. Mirrors
|
|
@@ -2796,7 +3252,8 @@ function slimObject(id, data) {
|
|
|
2796
3252
|
timestamp: data["timestamp"],
|
|
2797
3253
|
className: data["className"],
|
|
2798
3254
|
...typeof data["frameId"] === "string" ? { frameId: data["frameId"] } : {},
|
|
2799
|
-
...typeof data["trackId"] === "string" ? { trackId: data["trackId"] } : {}
|
|
3255
|
+
...typeof data["trackId"] === "string" ? { trackId: data["trackId"] } : {},
|
|
3256
|
+
...typeof data["importance"] === "number" ? { importance: data["importance"] } : {}
|
|
2800
3257
|
};
|
|
2801
3258
|
if (typeof data["label"] === "string") return {
|
|
2802
3259
|
...base,
|
|
@@ -2826,6 +3283,17 @@ function slimAudio(id, data) {
|
|
|
2826
3283
|
}
|
|
2827
3284
|
return base;
|
|
2828
3285
|
}
|
|
3286
|
+
function bboxAreaFrac(data) {
|
|
3287
|
+
const bbox = data["bbox"];
|
|
3288
|
+
const fw = data["frameWidth"];
|
|
3289
|
+
const fh = data["frameHeight"];
|
|
3290
|
+
if (bbox === null || typeof bbox !== "object") return 0;
|
|
3291
|
+
if (typeof fw !== "number" || typeof fh !== "number" || fw <= 0 || fh <= 0) return 0;
|
|
3292
|
+
const w = "w" in bbox && typeof bbox.w === "number" ? bbox.w : 0;
|
|
3293
|
+
const h = "h" in bbox && typeof bbox.h === "number" ? bbox.h : 0;
|
|
3294
|
+
if (w <= 0 || h <= 0) return 0;
|
|
3295
|
+
return w * h / (fw * fh);
|
|
3296
|
+
}
|
|
2829
3297
|
function stripNulls(data) {
|
|
2830
3298
|
const out = {};
|
|
2831
3299
|
for (const [k, v] of Object.entries(data)) if (v !== null) out[k] = v;
|
|
@@ -2972,7 +3440,9 @@ var EventMediaDispatcher = class {
|
|
|
2972
3440
|
}
|
|
2973
3441
|
async captureForFrame(input) {
|
|
2974
3442
|
const { deviceId, frameHandle, events, trackFrames } = input;
|
|
2975
|
-
|
|
3443
|
+
const snapshots = input.snapshots ?? [];
|
|
3444
|
+
const empty = { storedSnapshots: [] };
|
|
3445
|
+
if (events.length === 0 && trackFrames.length === 0 && snapshots.length === 0) return empty;
|
|
2976
3446
|
let decoded;
|
|
2977
3447
|
try {
|
|
2978
3448
|
decoded = await resolveFrame(frameHandle, {
|
|
@@ -2989,7 +3459,7 @@ var EventMediaDispatcher = class {
|
|
|
2989
3459
|
error: String(err)
|
|
2990
3460
|
}
|
|
2991
3461
|
});
|
|
2992
|
-
return;
|
|
3462
|
+
return empty;
|
|
2993
3463
|
}
|
|
2994
3464
|
if (!decoded) {
|
|
2995
3465
|
this.deps.logger.debug("event media: frame recycled before resolve", {
|
|
@@ -2999,7 +3469,7 @@ var EventMediaDispatcher = class {
|
|
|
2999
3469
|
shmId: frameHandle.shmId
|
|
3000
3470
|
}
|
|
3001
3471
|
});
|
|
3002
|
-
return;
|
|
3472
|
+
return empty;
|
|
3003
3473
|
}
|
|
3004
3474
|
if (decoded.format !== "rgb") {
|
|
3005
3475
|
this.deps.logger.debug("event media: resolved frame is not RGB", {
|
|
@@ -3009,13 +3479,87 @@ var EventMediaDispatcher = class {
|
|
|
3009
3479
|
format: decoded.format
|
|
3010
3480
|
}
|
|
3011
3481
|
});
|
|
3012
|
-
return;
|
|
3482
|
+
return empty;
|
|
3013
3483
|
}
|
|
3014
3484
|
const frameData = Buffer.isBuffer(decoded.data) ? decoded.data : Buffer.from(decoded.data);
|
|
3015
3485
|
const fw = decoded.width;
|
|
3016
3486
|
const fh = decoded.height;
|
|
3017
3487
|
for (const ev of events) await this.writeEventMedia(deviceId, frameData, fw, fh, ev, input.cropPadding);
|
|
3018
3488
|
for (const tf of trackFrames) await this.writeTrackFrame(deviceId, frameData, fw, fh, tf);
|
|
3489
|
+
const storedSnapshots = [];
|
|
3490
|
+
for (const sn of snapshots) {
|
|
3491
|
+
const stored = await this.writeTrackSnapshot(deviceId, frameData, fw, fh, sn);
|
|
3492
|
+
if (stored) storedSnapshots.push(stored);
|
|
3493
|
+
}
|
|
3494
|
+
return { storedSnapshots };
|
|
3495
|
+
}
|
|
3496
|
+
/**
|
|
3497
|
+
* Periodic per-track media (§5). Encodes the boxed frame ONCE and fans it out
|
|
3498
|
+
* to whichever of the three destinations is requested: an appended `snapshot`
|
|
3499
|
+
* (timeline filmstrip), the rolling `lastFrame` (overwrite), and the best
|
|
3500
|
+
* `thumbnail` (overwrite). Returns the appended snapshot for TrackStore wiring
|
|
3501
|
+
* (null when `appendSnapshot` is false or the encode failed).
|
|
3502
|
+
*/
|
|
3503
|
+
async writeTrackSnapshot(deviceId, frameData, fw, fh, sn) {
|
|
3504
|
+
if (!sn.appendSnapshot && !sn.rollingLastFrame && !sn.bestThumbnail) return null;
|
|
3505
|
+
let boxed;
|
|
3506
|
+
try {
|
|
3507
|
+
boxed = await drawBoxedFrame(frameData, fw, fh, [{
|
|
3508
|
+
...sn.bbox,
|
|
3509
|
+
...sn.label ? { label: sn.label } : {}
|
|
3510
|
+
}], { quality: MEDIA_QUALITY });
|
|
3511
|
+
} catch (err) {
|
|
3512
|
+
this.deps.logger.warn("event media: track snapshot encode failed", {
|
|
3513
|
+
tags: { deviceId },
|
|
3514
|
+
meta: {
|
|
3515
|
+
deviceId,
|
|
3516
|
+
trackId: sn.trackId,
|
|
3517
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3518
|
+
}
|
|
3519
|
+
});
|
|
3520
|
+
return null;
|
|
3521
|
+
}
|
|
3522
|
+
let stored = null;
|
|
3523
|
+
if (sn.appendSnapshot) try {
|
|
3524
|
+
const mediaKey = await this.deps.mediaStore.put({
|
|
3525
|
+
deviceId,
|
|
3526
|
+
ownerKind: "track",
|
|
3527
|
+
ownerId: sn.trackId,
|
|
3528
|
+
kind: "snapshot",
|
|
3529
|
+
timestamp: sn.timestamp,
|
|
3530
|
+
data: boxed
|
|
3531
|
+
});
|
|
3532
|
+
stored = {
|
|
3533
|
+
trackId: sn.trackId,
|
|
3534
|
+
mediaKey,
|
|
3535
|
+
timestamp: sn.timestamp,
|
|
3536
|
+
bbox: sn.bbox
|
|
3537
|
+
};
|
|
3538
|
+
} catch {}
|
|
3539
|
+
if (sn.rollingLastFrame) await this.replaceKind(deviceId, sn.trackId, "lastFrame", sn.timestamp, boxed);
|
|
3540
|
+
if (sn.bestThumbnail) await this.replaceKind(deviceId, sn.trackId, "thumbnail", sn.timestamp, boxed);
|
|
3541
|
+
return stored;
|
|
3542
|
+
}
|
|
3543
|
+
async replaceKind(deviceId, trackId, kind, timestamp, data) {
|
|
3544
|
+
try {
|
|
3545
|
+
await this.deps.mediaStore.putReplacing({
|
|
3546
|
+
deviceId,
|
|
3547
|
+
ownerKind: "track",
|
|
3548
|
+
ownerId: trackId,
|
|
3549
|
+
kind,
|
|
3550
|
+
timestamp,
|
|
3551
|
+
data
|
|
3552
|
+
});
|
|
3553
|
+
} catch (err) {
|
|
3554
|
+
this.deps.logger.debug(`event media: ${kind} replace failed`, {
|
|
3555
|
+
tags: { deviceId },
|
|
3556
|
+
meta: {
|
|
3557
|
+
deviceId,
|
|
3558
|
+
trackId,
|
|
3559
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3560
|
+
}
|
|
3561
|
+
});
|
|
3562
|
+
}
|
|
3019
3563
|
}
|
|
3020
3564
|
async writeEventMedia(deviceId, frameData, fw, fh, ev, cropPadding) {
|
|
3021
3565
|
const box = {
|
|
@@ -4010,11 +4554,20 @@ function resolveFaceSettings(raw) {
|
|
|
4010
4554
|
* its default — parse never throws). Mirrors `face-settings` /
|
|
4011
4555
|
* `audio-detection-settings`.
|
|
4012
4556
|
*/
|
|
4013
|
-
var MediaSettingsSchema = object({
|
|
4014
|
-
/** Fractional padding added around a detection bbox before cropping.
|
|
4015
|
-
* 0 = tight crop, 0.15 = expand each side by 15% of the box dimension,
|
|
4016
|
-
* max 2 (200%). */
|
|
4017
|
-
cropPadding: number().min(0).max(2).default(.15)
|
|
4557
|
+
var MediaSettingsSchema = object({
|
|
4558
|
+
/** Fractional padding added around a detection bbox before cropping.
|
|
4559
|
+
* 0 = tight crop, 0.15 = expand each side by 15% of the box dimension,
|
|
4560
|
+
* max 2 (200%). */
|
|
4561
|
+
cropPadding: number().min(0).max(2).default(.15),
|
|
4562
|
+
/** Master switch for periodic per-track snapshots (the timeline filmstrip +
|
|
4563
|
+
* the rolling `lastFrame` + the best `thumbnail`). When false, only the
|
|
4564
|
+
* per-track `firstFrame` and per-event media are produced. */
|
|
4565
|
+
saveThumbnails: boolean().default(true),
|
|
4566
|
+
/** Cadence (ms) for the periodic per-track `snapshot` + rolling `lastFrame`.
|
|
4567
|
+
* A snapshot is captured for an active track only after this much wall-clock
|
|
4568
|
+
* has elapsed since its previous one. */
|
|
4569
|
+
snapshotIntervalMs: number().int().min(500).max(6e4).default(5e3)
|
|
4570
|
+
});
|
|
4018
4571
|
var MEDIA_DEFAULTS = MediaSettingsSchema.parse({});
|
|
4019
4572
|
/**
|
|
4020
4573
|
* Resolve a per-device store blob into typed media settings. Unknown/invalid
|
|
@@ -4025,7 +4578,11 @@ function resolveMediaSettings(raw) {
|
|
|
4025
4578
|
const parsed = MediaSettingsSchema.shape[key].safeParse(raw[key]);
|
|
4026
4579
|
return parsed.success ? parsed.data : MEDIA_DEFAULTS[key];
|
|
4027
4580
|
};
|
|
4028
|
-
return {
|
|
4581
|
+
return {
|
|
4582
|
+
cropPadding: pick("cropPadding"),
|
|
4583
|
+
saveThumbnails: pick("saveThumbnails"),
|
|
4584
|
+
snapshotIntervalMs: pick("snapshotIntervalMs")
|
|
4585
|
+
};
|
|
4029
4586
|
}
|
|
4030
4587
|
//#endregion
|
|
4031
4588
|
//#region src/pipeline-analytics/store/identity-store.ts
|
|
@@ -4356,6 +4913,14 @@ var FACE_COLUMNS = [
|
|
|
4356
4913
|
{
|
|
4357
4914
|
name: "assignedSampleId",
|
|
4358
4915
|
type: "TEXT"
|
|
4916
|
+
},
|
|
4917
|
+
{
|
|
4918
|
+
name: "keyFrameMediaKey",
|
|
4919
|
+
type: "TEXT"
|
|
4920
|
+
},
|
|
4921
|
+
{
|
|
4922
|
+
name: "faceBbox",
|
|
4923
|
+
type: "JSON"
|
|
4359
4924
|
}
|
|
4360
4925
|
];
|
|
4361
4926
|
var FACE_INDEXES = [{
|
|
@@ -4428,7 +4993,9 @@ var FaceStore = class {
|
|
|
4428
4993
|
assigned: Boolean(r.data.assigned),
|
|
4429
4994
|
mediaKey: data.mediaKey ?? void 0,
|
|
4430
4995
|
recognizedIdentityId: data.recognizedIdentityId ?? void 0,
|
|
4431
|
-
assignedSampleId: data.assignedSampleId ?? void 0
|
|
4996
|
+
assignedSampleId: data.assignedSampleId ?? void 0,
|
|
4997
|
+
keyFrameMediaKey: data.keyFrameMediaKey ?? void 0,
|
|
4998
|
+
faceBbox: data.faceBbox ?? void 0
|
|
4432
4999
|
};
|
|
4433
5000
|
}).filter((f) => !f.assigned);
|
|
4434
5001
|
}
|
|
@@ -4592,8 +5159,11 @@ var FaceStore = class {
|
|
|
4592
5159
|
id: faceId,
|
|
4593
5160
|
...data,
|
|
4594
5161
|
assigned: Boolean(raw.assigned),
|
|
5162
|
+
mediaKey: data.mediaKey ?? void 0,
|
|
4595
5163
|
recognizedIdentityId: data.recognizedIdentityId ?? void 0,
|
|
4596
|
-
assignedSampleId: data.assignedSampleId ?? void 0
|
|
5164
|
+
assignedSampleId: data.assignedSampleId ?? void 0,
|
|
5165
|
+
keyFrameMediaKey: data.keyFrameMediaKey ?? void 0,
|
|
5166
|
+
faceBbox: data.faceBbox ?? void 0
|
|
4597
5167
|
};
|
|
4598
5168
|
}
|
|
4599
5169
|
/**
|
|
@@ -4637,7 +5207,9 @@ var FaceStore = class {
|
|
|
4637
5207
|
assigned: Boolean(r.data.assigned),
|
|
4638
5208
|
mediaKey: data.mediaKey ?? void 0,
|
|
4639
5209
|
recognizedIdentityId: data.recognizedIdentityId ?? void 0,
|
|
4640
|
-
assignedSampleId: data.assignedSampleId ?? void 0
|
|
5210
|
+
assignedSampleId: data.assignedSampleId ?? void 0,
|
|
5211
|
+
keyFrameMediaKey: data.keyFrameMediaKey ?? void 0,
|
|
5212
|
+
faceBbox: data.faceBbox ?? void 0
|
|
4641
5213
|
};
|
|
4642
5214
|
});
|
|
4643
5215
|
const filterMode = input.filter ?? "all";
|
|
@@ -4702,6 +5274,10 @@ var OBJECT_EMBEDDING_COLUMNS = [
|
|
|
4702
5274
|
{
|
|
4703
5275
|
name: "mediaKey",
|
|
4704
5276
|
type: "TEXT"
|
|
5277
|
+
},
|
|
5278
|
+
{
|
|
5279
|
+
name: "keyFrameMediaKey",
|
|
5280
|
+
type: "TEXT"
|
|
4705
5281
|
}
|
|
4706
5282
|
];
|
|
4707
5283
|
var ObjectEmbeddingStore = class {
|
|
@@ -4748,7 +5324,8 @@ var ObjectEmbeddingStore = class {
|
|
|
4748
5324
|
modelId: input.modelId,
|
|
4749
5325
|
dim: input.embedding.length,
|
|
4750
5326
|
confidence: input.confidence,
|
|
4751
|
-
...input.mediaKey !== void 0 ? { mediaKey: input.mediaKey } : {}
|
|
5327
|
+
...input.mediaKey !== void 0 ? { mediaKey: input.mediaKey } : {},
|
|
5328
|
+
...input.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: input.keyFrameMediaKey } : {}
|
|
4752
5329
|
};
|
|
4753
5330
|
try {
|
|
4754
5331
|
await this.store.set.mutate({
|
|
@@ -4786,7 +5363,8 @@ var ObjectEmbeddingStore = class {
|
|
|
4786
5363
|
return {
|
|
4787
5364
|
id: r.id,
|
|
4788
5365
|
...data,
|
|
4789
|
-
mediaKey: data.mediaKey ?? void 0
|
|
5366
|
+
mediaKey: data.mediaKey ?? void 0,
|
|
5367
|
+
keyFrameMediaKey: data.keyFrameMediaKey ?? void 0
|
|
4790
5368
|
};
|
|
4791
5369
|
});
|
|
4792
5370
|
}
|
|
@@ -4915,12 +5493,22 @@ function updateTrackAggregate(prev, match, opts) {
|
|
|
4915
5493
|
}
|
|
4916
5494
|
//#endregion
|
|
4917
5495
|
//#region src/pipeline-analytics/face-recognizer.ts
|
|
5496
|
+
/** At most one "dropping imageless track" log per this interval, per recognizer. */
|
|
5497
|
+
var FACE_IMAGELESS_LOG_THROTTLE_MS = 6e4;
|
|
4918
5498
|
var FaceRecognizer = class {
|
|
4919
5499
|
deps;
|
|
4920
5500
|
gallery = [];
|
|
4921
5501
|
names = /* @__PURE__ */ new Map();
|
|
4922
5502
|
aggregates = /* @__PURE__ */ new Map();
|
|
4923
5503
|
bestFace = /* @__PURE__ */ new Map();
|
|
5504
|
+
/** The ONE best-detection-per-track policy, shared with the best-frame path
|
|
5505
|
+
* (`index.ts`). Face params: no hysteresis / no rate-limit — always keep the
|
|
5506
|
+
* true highest-confidence face (holding a buffer in memory is cheap, and a
|
|
5507
|
+
* track may last well under the best-frame rate-limit window). */
|
|
5508
|
+
bestTracker = new BestDetectionTracker();
|
|
5509
|
+
/** Throttle for the "dropping imageless track" log — one line per minute at
|
|
5510
|
+
* most, so a busy scene that never produces a face crop can't flood logs. */
|
|
5511
|
+
lastImagelessLogAt = 0;
|
|
4924
5512
|
constructor(deps) {
|
|
4925
5513
|
this.deps = deps;
|
|
4926
5514
|
}
|
|
@@ -4954,55 +5542,25 @@ var FaceRecognizer = class {
|
|
|
4954
5542
|
threshold: settings.similarityThreshold,
|
|
4955
5543
|
margin: settings.margin
|
|
4956
5544
|
}) : /* @__PURE__ */ new Map();
|
|
5545
|
+
const labelWork = [];
|
|
4957
5546
|
for (const c of candidates) {
|
|
4958
5547
|
const match = matches.get(c.trackId) ?? null;
|
|
4959
5548
|
const { state, changed } = updateTrackAggregate(this.aggregates.get(c.trackId) ?? EMPTY_AGGREGATE, match, { confirmFrames: settings.confirmFrames });
|
|
4960
5549
|
this.aggregates.set(c.trackId, state);
|
|
4961
|
-
if (changed && state.assignedIdentityId !== null) {
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
trackId: c.trackId
|
|
4967
|
-
},
|
|
4968
|
-
meta: {
|
|
4969
|
-
identityId: state.assignedIdentityId,
|
|
4970
|
-
name: name ?? null,
|
|
4971
|
-
score: match?.score ?? null
|
|
4972
|
-
}
|
|
4973
|
-
});
|
|
4974
|
-
if (name !== void 0) {
|
|
4975
|
-
try {
|
|
4976
|
-
await this.deps.trackStore.setLabel(c.trackId, name);
|
|
4977
|
-
} catch (err) {
|
|
4978
|
-
this.deps.logger.warn("setLabel failed", {
|
|
4979
|
-
tags: { deviceId: input.deviceId },
|
|
4980
|
-
meta: {
|
|
4981
|
-
trackId: c.trackId,
|
|
4982
|
-
error: String(err)
|
|
4983
|
-
}
|
|
4984
|
-
});
|
|
4985
|
-
}
|
|
4986
|
-
try {
|
|
4987
|
-
await this.deps.eventStore.setLabelForTrack(c.trackId, name);
|
|
4988
|
-
} catch (err) {
|
|
4989
|
-
this.deps.logger.warn("setLabelForTrack failed", {
|
|
4990
|
-
tags: { deviceId: input.deviceId },
|
|
4991
|
-
meta: {
|
|
4992
|
-
trackId: c.trackId,
|
|
4993
|
-
error: String(err)
|
|
4994
|
-
}
|
|
4995
|
-
});
|
|
4996
|
-
}
|
|
4997
|
-
}
|
|
4998
|
-
}
|
|
5550
|
+
if (changed && state.assignedIdentityId !== null) labelWork.push({
|
|
5551
|
+
trackId: c.trackId,
|
|
5552
|
+
assignedIdentityId: state.assignedIdentityId,
|
|
5553
|
+
matchScore: match?.score ?? null
|
|
5554
|
+
});
|
|
4999
5555
|
const recognizedIdentityId = state.assignedIdentityId ?? match?.identityId ?? void 0;
|
|
5000
5556
|
const held = this.bestFace.get(c.trackId);
|
|
5001
|
-
|
|
5557
|
+
const isNewBest = this.bestTracker.observe(c.trackId, c.confidence, input.timestamp);
|
|
5558
|
+
const needsCrop = held !== void 0 && held.crop === void 0;
|
|
5559
|
+
if (isNewBest || needsCrop) {
|
|
5002
5560
|
const cropBbox = c.faceBbox ?? c.bbox;
|
|
5003
5561
|
let crop;
|
|
5004
|
-
if (input.frameHandle !== void 0) try {
|
|
5005
|
-
crop = await this.deps.captureCrop(input.frameHandle,
|
|
5562
|
+
if (c.faceBbox !== void 0 && input.frameHandle !== void 0) try {
|
|
5563
|
+
crop = await this.deps.captureCrop(input.frameHandle, c.faceBbox, input.frameWidth, input.frameHeight, input.cropPadding) ?? void 0;
|
|
5006
5564
|
} catch (err) {
|
|
5007
5565
|
this.deps.logger.debug("FaceRecognizer crop capture failed", {
|
|
5008
5566
|
tags: { deviceId: input.deviceId },
|
|
@@ -5012,14 +5570,20 @@ var FaceRecognizer = class {
|
|
|
5012
5570
|
}
|
|
5013
5571
|
});
|
|
5014
5572
|
}
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5573
|
+
if (isNewBest) {
|
|
5574
|
+
const bestCrop = crop ?? held?.crop;
|
|
5575
|
+
this.bestFace.set(c.trackId, {
|
|
5576
|
+
score: c.confidence,
|
|
5577
|
+
embedding: c.embedding,
|
|
5578
|
+
embeddingModelId: c.embeddingModelId,
|
|
5579
|
+
bbox: cropBbox,
|
|
5580
|
+
timestamp: input.timestamp,
|
|
5581
|
+
...bestCrop !== void 0 ? { crop: bestCrop } : {},
|
|
5582
|
+
...recognizedIdentityId !== void 0 ? { recognizedIdentityId } : {}
|
|
5583
|
+
});
|
|
5584
|
+
} else if (crop !== void 0 && held !== void 0) this.bestFace.set(c.trackId, {
|
|
5585
|
+
...held,
|
|
5586
|
+
crop
|
|
5023
5587
|
});
|
|
5024
5588
|
this.deps.logger.debug("face: best-face held", {
|
|
5025
5589
|
tags: {
|
|
@@ -5028,15 +5592,64 @@ var FaceRecognizer = class {
|
|
|
5028
5592
|
},
|
|
5029
5593
|
meta: {
|
|
5030
5594
|
score: c.confidence,
|
|
5031
|
-
|
|
5595
|
+
isNewBest,
|
|
5596
|
+
hasCrop: this.bestFace.get(c.trackId)?.crop !== void 0,
|
|
5032
5597
|
recognizedIdentityId: recognizedIdentityId ?? null
|
|
5033
5598
|
}
|
|
5034
5599
|
});
|
|
5035
|
-
} else if (recognizedIdentityId !== void 0 && held.recognizedIdentityId === void 0) this.bestFace.set(c.trackId, {
|
|
5600
|
+
} else if (held !== void 0 && recognizedIdentityId !== void 0 && held.recognizedIdentityId === void 0) this.bestFace.set(c.trackId, {
|
|
5036
5601
|
...held,
|
|
5037
5602
|
recognizedIdentityId
|
|
5038
5603
|
});
|
|
5039
5604
|
}
|
|
5605
|
+
for (const work of labelWork) {
|
|
5606
|
+
const name = this.names.get(work.assignedIdentityId);
|
|
5607
|
+
this.deps.logger.info("face: identity assigned", {
|
|
5608
|
+
tags: {
|
|
5609
|
+
deviceId: input.deviceId,
|
|
5610
|
+
trackId: work.trackId
|
|
5611
|
+
},
|
|
5612
|
+
meta: {
|
|
5613
|
+
identityId: work.assignedIdentityId,
|
|
5614
|
+
name: name ?? null,
|
|
5615
|
+
score: work.matchScore
|
|
5616
|
+
}
|
|
5617
|
+
});
|
|
5618
|
+
if (name === void 0) continue;
|
|
5619
|
+
try {
|
|
5620
|
+
await this.deps.trackStore.setLabel(work.trackId, name);
|
|
5621
|
+
} catch (err) {
|
|
5622
|
+
this.deps.logger.warn("setLabel failed", {
|
|
5623
|
+
tags: { deviceId: input.deviceId },
|
|
5624
|
+
meta: {
|
|
5625
|
+
trackId: work.trackId,
|
|
5626
|
+
error: String(err)
|
|
5627
|
+
}
|
|
5628
|
+
});
|
|
5629
|
+
}
|
|
5630
|
+
try {
|
|
5631
|
+
await this.deps.eventStore.setLabelForTrack(work.trackId, name);
|
|
5632
|
+
} catch (err) {
|
|
5633
|
+
this.deps.logger.warn("setLabelForTrack failed", {
|
|
5634
|
+
tags: { deviceId: input.deviceId },
|
|
5635
|
+
meta: {
|
|
5636
|
+
trackId: work.trackId,
|
|
5637
|
+
error: String(err)
|
|
5638
|
+
}
|
|
5639
|
+
});
|
|
5640
|
+
}
|
|
5641
|
+
try {
|
|
5642
|
+
await this.deps.recomputeImportance?.(work.trackId);
|
|
5643
|
+
} catch (err) {
|
|
5644
|
+
this.deps.logger.warn("recomputeImportance failed", {
|
|
5645
|
+
tags: { deviceId: input.deviceId },
|
|
5646
|
+
meta: {
|
|
5647
|
+
trackId: work.trackId,
|
|
5648
|
+
error: String(err)
|
|
5649
|
+
}
|
|
5650
|
+
});
|
|
5651
|
+
}
|
|
5652
|
+
}
|
|
5040
5653
|
}
|
|
5041
5654
|
/**
|
|
5042
5655
|
* Persist the held best face for a finished track as ONE FaceStore buffer
|
|
@@ -5047,6 +5660,7 @@ var FaceRecognizer = class {
|
|
|
5047
5660
|
const held = this.bestFace.get(trackId);
|
|
5048
5661
|
this.aggregates.delete(trackId);
|
|
5049
5662
|
this.bestFace.delete(trackId);
|
|
5663
|
+
this.bestTracker.delete(trackId);
|
|
5050
5664
|
if (held === void 0) {
|
|
5051
5665
|
this.deps.logger.debug("face: track ended without a held face", { tags: {
|
|
5052
5666
|
deviceId,
|
|
@@ -5054,9 +5668,23 @@ var FaceRecognizer = class {
|
|
|
5054
5668
|
} });
|
|
5055
5669
|
return;
|
|
5056
5670
|
}
|
|
5671
|
+
if (held.crop === void 0) {
|
|
5672
|
+
const now = Date.now();
|
|
5673
|
+
if (now - this.lastImagelessLogAt >= FACE_IMAGELESS_LOG_THROTTLE_MS) {
|
|
5674
|
+
this.lastImagelessLogAt = now;
|
|
5675
|
+
this.deps.logger.info("face: dropping imageless track (no crop captured)", {
|
|
5676
|
+
tags: {
|
|
5677
|
+
deviceId,
|
|
5678
|
+
trackId
|
|
5679
|
+
},
|
|
5680
|
+
meta: { score: held.score }
|
|
5681
|
+
});
|
|
5682
|
+
}
|
|
5683
|
+
return;
|
|
5684
|
+
}
|
|
5057
5685
|
const faceId = `face-${trackId}`;
|
|
5058
5686
|
let mediaKey;
|
|
5059
|
-
|
|
5687
|
+
try {
|
|
5060
5688
|
mediaKey = await this.deps.mediaStore.put({
|
|
5061
5689
|
deviceId,
|
|
5062
5690
|
ownerKind: "face",
|
|
@@ -5074,6 +5702,17 @@ var FaceRecognizer = class {
|
|
|
5074
5702
|
}
|
|
5075
5703
|
});
|
|
5076
5704
|
}
|
|
5705
|
+
if (mediaKey === void 0) {
|
|
5706
|
+
this.deps.logger.warn("face: crop store failed — dropping face row", {
|
|
5707
|
+
tags: {
|
|
5708
|
+
deviceId,
|
|
5709
|
+
trackId
|
|
5710
|
+
},
|
|
5711
|
+
meta: { faceId }
|
|
5712
|
+
});
|
|
5713
|
+
return;
|
|
5714
|
+
}
|
|
5715
|
+
const keyFrameMediaKey = this.deps.getKeyFrameMediaKey?.(trackId);
|
|
5077
5716
|
try {
|
|
5078
5717
|
await this.deps.faceStore.insert({
|
|
5079
5718
|
id: faceId,
|
|
@@ -5081,9 +5720,11 @@ var FaceRecognizer = class {
|
|
|
5081
5720
|
trackId,
|
|
5082
5721
|
timestamp: held.timestamp,
|
|
5083
5722
|
embedding: held.embedding,
|
|
5084
|
-
|
|
5723
|
+
mediaKey,
|
|
5085
5724
|
...held.recognizedIdentityId !== void 0 ? { recognizedIdentityId: held.recognizedIdentityId } : {},
|
|
5086
|
-
assigned: false
|
|
5725
|
+
assigned: false,
|
|
5726
|
+
faceBbox: held.bbox,
|
|
5727
|
+
...keyFrameMediaKey !== void 0 ? { keyFrameMediaKey } : {}
|
|
5087
5728
|
});
|
|
5088
5729
|
this.deps.logger.info("face: buffered to gallery", {
|
|
5089
5730
|
tags: {
|
|
@@ -5092,7 +5733,7 @@ var FaceRecognizer = class {
|
|
|
5092
5733
|
},
|
|
5093
5734
|
meta: {
|
|
5094
5735
|
faceId,
|
|
5095
|
-
hasCrop:
|
|
5736
|
+
hasCrop: true,
|
|
5096
5737
|
recognizedIdentityId: held.recognizedIdentityId ?? null,
|
|
5097
5738
|
score: held.score
|
|
5098
5739
|
}
|
|
@@ -5424,6 +6065,38 @@ var PlateRecognizer = class {
|
|
|
5424
6065
|
}
|
|
5425
6066
|
};
|
|
5426
6067
|
//#endregion
|
|
6068
|
+
//#region src/shared/frame/shared-frame-resolver.ts
|
|
6069
|
+
function frameHandleKey(h) {
|
|
6070
|
+
return `${h.nodeId}:${h.shmId}:${h.slot}:${h.seq}`;
|
|
6071
|
+
}
|
|
6072
|
+
function createSharedFrameResolver(resolve) {
|
|
6073
|
+
let cache = null;
|
|
6074
|
+
return (handle) => {
|
|
6075
|
+
const key = frameHandleKey(handle);
|
|
6076
|
+
if (cache === null || cache.key !== key) cache = {
|
|
6077
|
+
key,
|
|
6078
|
+
value: resolve(handle)
|
|
6079
|
+
};
|
|
6080
|
+
return cache.value;
|
|
6081
|
+
};
|
|
6082
|
+
}
|
|
6083
|
+
//#endregion
|
|
6084
|
+
//#region src/shared/frame/encode-crop.ts
|
|
6085
|
+
/**
|
|
6086
|
+
* JPEG-encode an already-cropped raw RGB (24-bit) buffer. Used for the
|
|
6087
|
+
* NATIVE-resolution crop path: the decode worker returns the exact ROI pixels
|
|
6088
|
+
* at native res, so there is nothing left to crop — only to encode to the same
|
|
6089
|
+
* JPEG contract `extractCrop` produces (quality 90). Kept separate from
|
|
6090
|
+
* `extractCrop` (which crops a FULL frame) so the native path never re-crops.
|
|
6091
|
+
*/
|
|
6092
|
+
async function encodeRgbCropToJpeg(bytes, width, height) {
|
|
6093
|
+
return sharp(Buffer.from(bytes), { raw: {
|
|
6094
|
+
width,
|
|
6095
|
+
height,
|
|
6096
|
+
channels: 3
|
|
6097
|
+
} }).jpeg({ quality: 90 }).toBuffer();
|
|
6098
|
+
}
|
|
6099
|
+
//#endregion
|
|
5427
6100
|
//#region src/pipeline-analytics/pipeline/event-child-crops.ts
|
|
5428
6101
|
/**
|
|
5429
6102
|
* Compute Intersection-over-Union between two (x,y,w,h) bounding boxes.
|
|
@@ -5722,6 +6395,24 @@ function createEventMediaHandler(deps) {
|
|
|
5722
6395
|
*/
|
|
5723
6396
|
var TTL_SWEEP_INTERVAL_MS = 5e3;
|
|
5724
6397
|
var SETTINGS_CACHE_TTL_MS = 5e3;
|
|
6398
|
+
/** §5 best-frame: a track's `thumbnail` is overwritten only when the current
|
|
6399
|
+
* detection confidence beats the held best by at least this margin (hysteresis
|
|
6400
|
+
* so jitter around a plateau doesn't churn the write). */
|
|
6401
|
+
var BEST_FRAME_HYSTERESIS = .05;
|
|
6402
|
+
/** §5 best-frame: at most one best-thumbnail capture per this interval per track. */
|
|
6403
|
+
var BEST_FRAME_MIN_GAP_MS = 2e3;
|
|
6404
|
+
/** Design B: cap (px) on the width of the native KEY FRAME (full-frame native
|
|
6405
|
+
* capture). Native resolution is the point, but a full 4K RGB surface over the
|
|
6406
|
+
* transport per new-best is wasteful for a web detail view — 1920px keeps a
|
|
6407
|
+
* sharp native frame while bounding the copy (a miss falls back to the
|
|
6408
|
+
* detection-res frame, which is already ≤640px). */
|
|
6409
|
+
var KEYFRAME_NATIVE_MAX_WIDTH = 1920;
|
|
6410
|
+
/** getKeyEvents: max completed tracks pulled from a window before importance
|
|
6411
|
+
* ranking. Ordering is by importance (not firstSeen) and legacy rows score on
|
|
6412
|
+
* read, so we over-fetch candidates and trim to `limit` after sorting. */
|
|
6413
|
+
var KEY_EVENT_CANDIDATE_CAP = 500;
|
|
6414
|
+
/** getKeyEvents: default page size when the caller omits `limit`. */
|
|
6415
|
+
var KEY_EVENT_DEFAULT_LIMIT = 50;
|
|
5725
6416
|
/** Cluster setting key (in the centralized addon store) selecting the SINGLE
|
|
5726
6417
|
* node that runs post-analysis (event/media/audio/motion generation). All
|
|
5727
6418
|
* other nodes are fully inert. No multi-node balancing. Default: the hub. */
|
|
@@ -5833,6 +6524,31 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
5833
6524
|
mediaCacheByDevice = /* @__PURE__ */ new Map();
|
|
5834
6525
|
/** Consecutive detector-dropout frames skipped per (deviceId, source) key. */
|
|
5835
6526
|
dropoutSkipsByKey = /* @__PURE__ */ new Map();
|
|
6527
|
+
/** Best (highest-confidence) frame per track — drives the single overwrite
|
|
6528
|
+
* `thumbnail` (§5 best-frame). Shares the ONE best-detection policy with the
|
|
6529
|
+
* face path (`face-recognizer.ts`); rate-limited here since each best-frame
|
|
6530
|
+
* capture re-encodes a full boxed frame. */
|
|
6531
|
+
bestFrameTracker = new BestDetectionTracker({
|
|
6532
|
+
hysteresis: BEST_FRAME_HYSTERESIS,
|
|
6533
|
+
minGapMs: BEST_FRAME_MIN_GAP_MS
|
|
6534
|
+
});
|
|
6535
|
+
/** Best (highest-confidence) CLIP-object detection per track — drives ONE
|
|
6536
|
+
* tight object-crop capture whose media key is written onto the embedding
|
|
6537
|
+
* row (so a semantic-search hit's thumbnail IS the embedded crop). Shares the
|
|
6538
|
+
* unified best-per-track decision (`TrackBestSelector`); the persistent
|
|
6539
|
+
* cross-restart "best" gate stays in `ObjectEmbeddingStore.upsertIfBetter`. */
|
|
6540
|
+
objectEmbeddingBestSelector = new TrackBestSelector();
|
|
6541
|
+
/** Design B: the track's shared native key-frame media key, captured at the
|
|
6542
|
+
* best-detection moment (object-embedding best path). Read by the face path
|
|
6543
|
+
* at track end so a face row links to the SAME single key frame. Cleared on
|
|
6544
|
+
* track end. */
|
|
6545
|
+
keyFrameKeyByTrackId = /* @__PURE__ */ new Map();
|
|
6546
|
+
/** The shared crop extractor (native-res first, detection-frame fallback),
|
|
6547
|
+
* captured in the constructor so `processFrame` can crop object thumbnails in
|
|
6548
|
+
* the same live-frame window as the face/plate/event-media captures. The
|
|
6549
|
+
* optional `maxWidth` caps the native crop width (used for the full-frame key
|
|
6550
|
+
* frame so a 4K native surface never floods the transport). */
|
|
6551
|
+
captureCrop = null;
|
|
5836
6552
|
shuttingDown = false;
|
|
5837
6553
|
/** True only on the cluster's designated post-processing node. When false the
|
|
5838
6554
|
* addon subscribes to NOTHING — fully inert (no event/media generation). */
|
|
@@ -5932,23 +6648,62 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
5932
6648
|
});
|
|
5933
6649
|
const ownNodeIdForFaces = ownNodeId;
|
|
5934
6650
|
const frameReadersForFaces = this.frameReaders;
|
|
5935
|
-
const
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
6651
|
+
const pipelineRunnerApi = api.pipelineRunner;
|
|
6652
|
+
const cropMetricLogger = logger.child("NativeCrop");
|
|
6653
|
+
let nativeHits = 0;
|
|
6654
|
+
let nativeFallbacks = 0;
|
|
6655
|
+
let lastCropMetricAt = 0;
|
|
6656
|
+
const NATIVE_CROP_METRIC_INTERVAL_MS = 3e4;
|
|
6657
|
+
const bumpCropMetric = (hit) => {
|
|
6658
|
+
if (hit) nativeHits += 1;
|
|
6659
|
+
else nativeFallbacks += 1;
|
|
6660
|
+
const now = Date.now();
|
|
6661
|
+
if (now - lastCropMetricAt < NATIVE_CROP_METRIC_INTERVAL_MS) return;
|
|
6662
|
+
lastCropMetricAt = now;
|
|
6663
|
+
cropMetricLogger.info("native-crop window", { meta: {
|
|
6664
|
+
nativeHits,
|
|
6665
|
+
detectionFrameFallbacks: nativeFallbacks
|
|
6666
|
+
} });
|
|
6667
|
+
};
|
|
6668
|
+
const tryNativeCrop = async (frameHandle, paddedNorm, maxWidth) => {
|
|
6669
|
+
if (!pipelineRunnerApi?.getNativeCrop) return null;
|
|
6670
|
+
try {
|
|
6671
|
+
const native = await pipelineRunnerApi.getNativeCrop.query({
|
|
6672
|
+
handle: frameHandle,
|
|
6673
|
+
bbox: paddedNorm,
|
|
6674
|
+
...maxWidth !== void 0 ? { maxWidth } : {}
|
|
6675
|
+
}, nodePin(frameHandle.nodeId));
|
|
6676
|
+
if (!native || native.width <= 0 || native.height <= 0) return null;
|
|
6677
|
+
return await encodeRgbCropToJpeg(Buffer.from(native.bytes), native.width, native.height);
|
|
6678
|
+
} catch (err) {
|
|
6679
|
+
cropMetricLogger.debug("native crop request failed — falling back", { meta: { error: errMsg(err) } });
|
|
6680
|
+
return null;
|
|
6681
|
+
}
|
|
6682
|
+
};
|
|
6683
|
+
const resolveFrameShared = createSharedFrameResolver((frameHandle) => resolveFrame(frameHandle, {
|
|
6684
|
+
ownNodeId: ownNodeIdForFaces,
|
|
6685
|
+
readers: frameReadersForFaces,
|
|
6686
|
+
getRemoteFrame
|
|
6687
|
+
}));
|
|
6688
|
+
const captureCrop = async (frameHandle, bbox, frameWidth, frameHeight, padding, maxWidth) => {
|
|
5943
6689
|
const paddedNorm = padBbox({
|
|
5944
6690
|
x: bbox.x / frameWidth,
|
|
5945
6691
|
y: bbox.y / frameHeight,
|
|
5946
6692
|
w: bbox.w / frameWidth,
|
|
5947
6693
|
h: bbox.h / frameHeight
|
|
5948
6694
|
}, padding);
|
|
5949
|
-
const
|
|
6695
|
+
const nativeCrop = await tryNativeCrop(frameHandle, paddedNorm, maxWidth);
|
|
6696
|
+
if (nativeCrop) {
|
|
6697
|
+
bumpCropMetric(true);
|
|
6698
|
+
return nativeCrop;
|
|
6699
|
+
}
|
|
6700
|
+
bumpCropMetric(false);
|
|
6701
|
+
const decoded = await resolveFrameShared(frameHandle);
|
|
6702
|
+
if (!decoded || decoded.format !== "rgb") return null;
|
|
6703
|
+
const { crop } = await extractCrop(Buffer.isBuffer(decoded.data) ? decoded.data : Buffer.from(decoded.data), decoded.width, decoded.height, paddedNorm);
|
|
5950
6704
|
return crop;
|
|
5951
6705
|
};
|
|
6706
|
+
this.captureCrop = captureCrop;
|
|
5952
6707
|
this.faceRecognizer = new FaceRecognizer({
|
|
5953
6708
|
identityStore: this.identityStore,
|
|
5954
6709
|
faceStore: this.faceStore,
|
|
@@ -5956,6 +6711,16 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
5956
6711
|
trackStore: this.trackStore,
|
|
5957
6712
|
eventStore: this.eventStore,
|
|
5958
6713
|
captureCrop,
|
|
6714
|
+
recomputeImportance: (trackId) => {
|
|
6715
|
+
const trackStore = this.trackStore;
|
|
6716
|
+
const eventStore = this.eventStore;
|
|
6717
|
+
if (!trackStore || !eventStore) return Promise.resolve();
|
|
6718
|
+
return recomputeTrackImportance({
|
|
6719
|
+
trackStore,
|
|
6720
|
+
eventStore
|
|
6721
|
+
}, trackId);
|
|
6722
|
+
},
|
|
6723
|
+
getKeyFrameMediaKey: (trackId) => this.keyFrameKeyByTrackId.get(trackId),
|
|
5959
6724
|
logger: logger.child("FaceRecognizer")
|
|
5960
6725
|
});
|
|
5961
6726
|
this.faceRecognizer.refreshGallery();
|
|
@@ -5993,7 +6758,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
5993
6758
|
try {
|
|
5994
6759
|
const handler = createEventMediaHandler({ getMedia: async (id) => {
|
|
5995
6760
|
try {
|
|
5996
|
-
return await this.
|
|
6761
|
+
return await this.readMediaByEventOrKey(id);
|
|
5997
6762
|
} catch (err) {
|
|
5998
6763
|
this.ctx.logger.warn("readEventThumbnail failed", { meta: {
|
|
5999
6764
|
eventId: id,
|
|
@@ -6376,6 +7141,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
6376
7141
|
this.processors.clear();
|
|
6377
7142
|
this.lastActiveTrackIds.clear();
|
|
6378
7143
|
this.dropoutSkipsByKey.clear();
|
|
7144
|
+
this.bestFrameTracker.clear();
|
|
7145
|
+
this.objectEmbeddingBestSelector.clear();
|
|
6379
7146
|
this.levelStateByDevice.clear();
|
|
6380
7147
|
this.settingsCacheByDevice.clear();
|
|
6381
7148
|
this.sensitivityCacheByDevice.clear();
|
|
@@ -6482,12 +7249,15 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
6482
7249
|
className: t.className,
|
|
6483
7250
|
source
|
|
6484
7251
|
} });
|
|
6485
|
-
if (this.eventMediaDispatcher && frameHandle)
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
7252
|
+
if (this.eventMediaDispatcher && frameHandle) {
|
|
7253
|
+
firstFrameTargets.push({
|
|
7254
|
+
trackId: id,
|
|
7255
|
+
timestamp: result.timestamp,
|
|
7256
|
+
bbox: { ...t.bbox },
|
|
7257
|
+
...t.label ? { label: t.label } : {}
|
|
7258
|
+
});
|
|
7259
|
+
this.trackStore.seedSnapshotClock(id, result.timestamp);
|
|
7260
|
+
}
|
|
6491
7261
|
this.ctx.eventBus.emit({
|
|
6492
7262
|
id: `pa-${randomUUID()}`,
|
|
6493
7263
|
timestamp: new Date(result.timestamp),
|
|
@@ -6528,16 +7298,18 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
6528
7298
|
} });
|
|
6529
7299
|
}
|
|
6530
7300
|
await Promise.all(result.objectEvents.map((e) => this.eventStore.insertObject(e)));
|
|
6531
|
-
|
|
6532
|
-
|
|
7301
|
+
const objectEmbeddingBests = [];
|
|
7302
|
+
if (this.objectEmbeddingStore) for (const t of result.tracked) {
|
|
7303
|
+
if (!isClipObjectEmbedding(t)) continue;
|
|
7304
|
+
if (this.objectEmbeddingBestSelector.observe({
|
|
6533
7305
|
trackId: t.trackId,
|
|
6534
|
-
|
|
6535
|
-
|
|
7306
|
+
confidence: t.confidence,
|
|
7307
|
+
atMs: result.timestamp,
|
|
6536
7308
|
className: t.className,
|
|
7309
|
+
bbox: t.bbox,
|
|
6537
7310
|
embedding: t.embedding,
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
});
|
|
7311
|
+
embeddingModelId: t.embeddingModelId
|
|
7312
|
+
})) objectEmbeddingBests.push(t);
|
|
6541
7313
|
}
|
|
6542
7314
|
const faceSettings = this.faceRecognizer ? await this.resolveDeviceFaceSettings(deviceId) : null;
|
|
6543
7315
|
const faceGloballyEnabled = this.faceRecognizer ? await this.resolveGlobalFaceEnabled() : false;
|
|
@@ -6560,11 +7332,13 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
6560
7332
|
let plateCrops = 0;
|
|
6561
7333
|
for (const crops of childCropsByEvent.values()) for (const c of crops) if (c.kind === "faceCrop") faceCrops += 1;
|
|
6562
7334
|
else plateCrops += 1;
|
|
6563
|
-
|
|
7335
|
+
const snapshotTargets = this.buildSnapshotTargets(result.tracked, result.timestamp, mediaSettings);
|
|
7336
|
+
if (eventTargets.length > 0 || firstFrameTargets.length > 0 || snapshotTargets.length > 0) {
|
|
6564
7337
|
log.info("media capture", { meta: {
|
|
6565
7338
|
source,
|
|
6566
7339
|
events: eventTargets.length,
|
|
6567
7340
|
trackFrames: firstFrameTargets.length,
|
|
7341
|
+
snapshots: snapshotTargets.length,
|
|
6568
7342
|
faceCrops,
|
|
6569
7343
|
plateCrops
|
|
6570
7344
|
} });
|
|
@@ -6573,8 +7347,20 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
6573
7347
|
frameHandle,
|
|
6574
7348
|
events: eventTargets,
|
|
6575
7349
|
trackFrames: firstFrameTargets,
|
|
7350
|
+
snapshots: snapshotTargets,
|
|
6576
7351
|
cropPadding: mediaSettings.cropPadding
|
|
6577
|
-
})
|
|
7352
|
+
}).then((res) => {
|
|
7353
|
+
for (const s of res.storedSnapshots) this.trackStore?.addSnapshot(s.trackId, {
|
|
7354
|
+
timestamp: s.timestamp,
|
|
7355
|
+
position: {
|
|
7356
|
+
x: s.bbox.x + s.bbox.w / 2,
|
|
7357
|
+
y: s.bbox.y + s.bbox.h / 2,
|
|
7358
|
+
timestamp: s.timestamp,
|
|
7359
|
+
bbox: s.bbox
|
|
7360
|
+
},
|
|
7361
|
+
mediaKey: s.mediaKey
|
|
7362
|
+
});
|
|
7363
|
+
}).catch(() => {});
|
|
6578
7364
|
}
|
|
6579
7365
|
}
|
|
6580
7366
|
if (this.faceRecognizer && faceGloballyEnabled && faceSettings) this.faceRecognizer.processFrame({
|
|
@@ -6596,6 +7382,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
6596
7382
|
cropPadding: mediaSettings.cropPadding,
|
|
6597
7383
|
...frameHandle !== void 0 ? { frameHandle } : {}
|
|
6598
7384
|
});
|
|
7385
|
+
if (objectEmbeddingBests.length > 0) this.persistObjectEmbeddingBests(deviceId, result.timestamp, objectEmbeddingBests, frameHandle, result.frameWidth, result.frameHeight, mediaSettings.cropPadding);
|
|
6599
7386
|
for (const e of result.objectEvents) this.ctx.eventBus.emit({
|
|
6600
7387
|
id: `pa-${e.id}`,
|
|
6601
7388
|
timestamp: new Date(e.timestamp),
|
|
@@ -6707,6 +7494,112 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
6707
7494
|
});
|
|
6708
7495
|
return settings;
|
|
6709
7496
|
}
|
|
7497
|
+
/**
|
|
7498
|
+
* §5 — decide which active tracks need periodic media THIS frame. Pure over
|
|
7499
|
+
* TrackStore.lastSnapshotAt + the per-track best-confidence map:
|
|
7500
|
+
* • `snapshot` (append) + `lastFrame` (rolling overwrite) fire together on
|
|
7501
|
+
* the snapshotIntervalMs cadence, gated by `saveThumbnails`;
|
|
7502
|
+
* • `thumbnail` (best) fires when confidence beats the held best by the
|
|
7503
|
+
* hysteresis margin, rate-limited to one per BEST_FRAME_MIN_GAP_MS, and is
|
|
7504
|
+
* NOT gated by saveThumbnails (a single best still-frame is always useful).
|
|
7505
|
+
* The per-track best-confidence map is updated here as a side effect.
|
|
7506
|
+
*/
|
|
7507
|
+
/**
|
|
7508
|
+
* Capture the tight object crop for each new-best CLIP track (native-res via
|
|
7509
|
+
* the shared extractor) and upsert the embedding row with that crop's media
|
|
7510
|
+
* key. The crop uses `putReplacing` so exactly ONE object crop is kept per
|
|
7511
|
+
* track (the current peak). The embedding is upserted even when no crop was
|
|
7512
|
+
* captured (no frame handle) so semantic search still works — the crop just
|
|
7513
|
+
* enhances the search-hit thumbnail. `upsertIfBetter` remains the durable
|
|
7514
|
+
* cross-restart best gate (R3). Best-effort; issued in the live-frame window.
|
|
7515
|
+
*/
|
|
7516
|
+
async persistObjectEmbeddingBests(deviceId, timestamp, bests, frameHandle, frameWidth, frameHeight, cropPadding) {
|
|
7517
|
+
const store = this.objectEmbeddingStore;
|
|
7518
|
+
if (!store) return;
|
|
7519
|
+
await Promise.all(bests.map(async (t) => {
|
|
7520
|
+
if (!isClipObjectEmbedding(t)) return;
|
|
7521
|
+
let mediaKey;
|
|
7522
|
+
let keyFrameMediaKey;
|
|
7523
|
+
if (frameHandle !== void 0 && this.captureCrop && this.mediaStore) {
|
|
7524
|
+
try {
|
|
7525
|
+
const crop = await this.captureCrop(frameHandle, t.bbox, frameWidth, frameHeight, cropPadding);
|
|
7526
|
+
if (crop) mediaKey = await this.mediaStore.putReplacing({
|
|
7527
|
+
deviceId,
|
|
7528
|
+
ownerKind: "track",
|
|
7529
|
+
ownerId: t.trackId,
|
|
7530
|
+
kind: "crop",
|
|
7531
|
+
timestamp,
|
|
7532
|
+
data: crop
|
|
7533
|
+
});
|
|
7534
|
+
} catch (err) {
|
|
7535
|
+
this.ctx.logger.debug("object-embedding crop capture failed", {
|
|
7536
|
+
tags: { deviceId },
|
|
7537
|
+
meta: {
|
|
7538
|
+
trackId: t.trackId,
|
|
7539
|
+
error: errMsg(err)
|
|
7540
|
+
}
|
|
7541
|
+
});
|
|
7542
|
+
}
|
|
7543
|
+
try {
|
|
7544
|
+
const keyFrame = await this.captureCrop(frameHandle, {
|
|
7545
|
+
x: 0,
|
|
7546
|
+
y: 0,
|
|
7547
|
+
w: frameWidth,
|
|
7548
|
+
h: frameHeight
|
|
7549
|
+
}, frameWidth, frameHeight, 0, KEYFRAME_NATIVE_MAX_WIDTH);
|
|
7550
|
+
if (keyFrame) {
|
|
7551
|
+
keyFrameMediaKey = await this.mediaStore.putReplacing({
|
|
7552
|
+
deviceId,
|
|
7553
|
+
ownerKind: "track",
|
|
7554
|
+
ownerId: t.trackId,
|
|
7555
|
+
kind: "keyFrame",
|
|
7556
|
+
timestamp,
|
|
7557
|
+
data: keyFrame
|
|
7558
|
+
});
|
|
7559
|
+
this.keyFrameKeyByTrackId.set(t.trackId, keyFrameMediaKey);
|
|
7560
|
+
}
|
|
7561
|
+
} catch (err) {
|
|
7562
|
+
this.ctx.logger.debug("key-frame capture failed", {
|
|
7563
|
+
tags: { deviceId },
|
|
7564
|
+
meta: {
|
|
7565
|
+
trackId: t.trackId,
|
|
7566
|
+
error: errMsg(err)
|
|
7567
|
+
}
|
|
7568
|
+
});
|
|
7569
|
+
}
|
|
7570
|
+
}
|
|
7571
|
+
await store.upsertIfBetter({
|
|
7572
|
+
trackId: t.trackId,
|
|
7573
|
+
deviceId,
|
|
7574
|
+
timestamp,
|
|
7575
|
+
className: t.className,
|
|
7576
|
+
embedding: t.embedding,
|
|
7577
|
+
modelId: t.embeddingModelId,
|
|
7578
|
+
confidence: t.confidence,
|
|
7579
|
+
...mediaKey !== void 0 ? { mediaKey } : {},
|
|
7580
|
+
...keyFrameMediaKey !== void 0 ? { keyFrameMediaKey } : {}
|
|
7581
|
+
});
|
|
7582
|
+
}));
|
|
7583
|
+
}
|
|
7584
|
+
buildSnapshotTargets(tracked, timestamp, media) {
|
|
7585
|
+
const targets = [];
|
|
7586
|
+
for (const t of tracked) {
|
|
7587
|
+
const lastSnap = this.trackStore.lastSnapshotAt(t.trackId);
|
|
7588
|
+
const dueSnapshot = media.saveThumbnails && lastSnap > 0 && timestamp - lastSnap >= media.snapshotIntervalMs;
|
|
7589
|
+
const isNewBest = this.bestFrameTracker.observe(t.trackId, t.confidence, timestamp);
|
|
7590
|
+
if (!dueSnapshot && !isNewBest) continue;
|
|
7591
|
+
targets.push({
|
|
7592
|
+
trackId: t.trackId,
|
|
7593
|
+
timestamp,
|
|
7594
|
+
bbox: { ...t.bbox },
|
|
7595
|
+
...t.label ? { label: t.label } : {},
|
|
7596
|
+
appendSnapshot: dueSnapshot,
|
|
7597
|
+
rollingLastFrame: dueSnapshot,
|
|
7598
|
+
bestThumbnail: isNewBest
|
|
7599
|
+
});
|
|
7600
|
+
}
|
|
7601
|
+
return targets;
|
|
7602
|
+
}
|
|
6710
7603
|
async handleAudioResult(data) {
|
|
6711
7604
|
if (this.shuttingDown) return;
|
|
6712
7605
|
const { deviceId, frame } = data;
|
|
@@ -6959,8 +7852,36 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
6959
7852
|
positions: t.positions.length
|
|
6960
7853
|
}
|
|
6961
7854
|
});
|
|
6962
|
-
this.faceRecognizer?.onTrackEnd(t.deviceId, t.trackId);
|
|
7855
|
+
const faceEnd = this.faceRecognizer?.onTrackEnd(t.deviceId, t.trackId);
|
|
7856
|
+
const dropKeyFrameKey = () => {
|
|
7857
|
+
this.keyFrameKeyByTrackId.delete(t.trackId);
|
|
7858
|
+
};
|
|
7859
|
+
if (faceEnd) faceEnd.finally(dropKeyFrameKey);
|
|
7860
|
+
else dropKeyFrameKey();
|
|
6963
7861
|
this.plateRecognizer?.onTrackEnd(t.deviceId, t.trackId);
|
|
7862
|
+
try {
|
|
7863
|
+
const peak = await this.eventStore?.peakForTrack(t.trackId);
|
|
7864
|
+
if (peak) {
|
|
7865
|
+
const { importance, reason } = computeImportance({
|
|
7866
|
+
peakConfidence: this.bestFrameTracker.peak(t.trackId)?.confidence ?? peak.peakConfidence,
|
|
7867
|
+
className: t.className,
|
|
7868
|
+
durationMs: duration,
|
|
7869
|
+
peakBboxAreaFrac: peak.peakBboxAreaFrac,
|
|
7870
|
+
totalDistance: t.totalDistance,
|
|
7871
|
+
zonesVisited: t.zonesVisited,
|
|
7872
|
+
...t.label !== void 0 ? { label: t.label } : {}
|
|
7873
|
+
});
|
|
7874
|
+
await this.trackStore?.setImportance(t.trackId, importance, reason, peak.bestEventId);
|
|
7875
|
+
if (peak.bestEventId !== void 0) await this.eventStore?.setImportanceForTrack(t.trackId, importance);
|
|
7876
|
+
}
|
|
7877
|
+
} catch (err) {
|
|
7878
|
+
this.ctx.logger.debug("importance scoring failed", { meta: {
|
|
7879
|
+
trackId: t.trackId,
|
|
7880
|
+
error: String(err)
|
|
7881
|
+
} });
|
|
7882
|
+
}
|
|
7883
|
+
this.bestFrameTracker.delete(t.trackId);
|
|
7884
|
+
this.objectEmbeddingBestSelector.delete(t.trackId);
|
|
6964
7885
|
this.ctx.eventBus.emit({
|
|
6965
7886
|
id: `pa-end-${t.trackId}`,
|
|
6966
7887
|
timestamp: new Date(t.lastSeen),
|
|
@@ -7183,6 +8104,32 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
7183
8104
|
if (input.projection !== "slim" || this.eventMediaBaseUrl === null) return events;
|
|
7184
8105
|
return this.withMediaUrl(events);
|
|
7185
8106
|
}
|
|
8107
|
+
async getKeyEvents(input) {
|
|
8108
|
+
const trackStore = this.trackStore;
|
|
8109
|
+
if (!trackStore) return [];
|
|
8110
|
+
const eventStore = this.eventStore;
|
|
8111
|
+
try {
|
|
8112
|
+
const candidates = await trackStore.queryHistorical({
|
|
8113
|
+
deviceId: input.deviceId,
|
|
8114
|
+
since: input.since,
|
|
8115
|
+
until: input.until,
|
|
8116
|
+
limit: KEY_EVENT_CANDIDATE_CAP
|
|
8117
|
+
});
|
|
8118
|
+
const peakLookup = (trackId) => eventStore ? eventStore.peakForTrack(trackId) : Promise.resolve({
|
|
8119
|
+
peakConfidence: 0,
|
|
8120
|
+
peakBboxAreaFrac: 0,
|
|
8121
|
+
bestEventId: void 0
|
|
8122
|
+
});
|
|
8123
|
+
return await rankKeyEvents(candidates, {
|
|
8124
|
+
limit: input.limit ?? KEY_EVENT_DEFAULT_LIMIT,
|
|
8125
|
+
...input.minImportance !== void 0 ? { minImportance: input.minImportance } : {},
|
|
8126
|
+
...input.classFilter !== void 0 ? { classFilter: input.classFilter } : {}
|
|
8127
|
+
}, peakLookup);
|
|
8128
|
+
} catch (err) {
|
|
8129
|
+
this.ctx.logger.debug("getKeyEvents failed", { meta: { error: String(err) } });
|
|
8130
|
+
return [];
|
|
8131
|
+
}
|
|
8132
|
+
}
|
|
7186
8133
|
async getAudioEvents(input) {
|
|
7187
8134
|
const events = await (this.eventStore?.queryAudio(input) ?? Promise.resolve([]));
|
|
7188
8135
|
if (input.projection !== "slim" || this.eventMediaBaseUrl === null) return events;
|
|
@@ -7225,6 +8172,12 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
7225
8172
|
className: input.classFilter
|
|
7226
8173
|
});
|
|
7227
8174
|
if (embeddingRows.length === 0) return [];
|
|
8175
|
+
const embeddingMediaKeyByTrackId = /* @__PURE__ */ new Map();
|
|
8176
|
+
const keyFrameKeyByTrackId = /* @__PURE__ */ new Map();
|
|
8177
|
+
for (const row of embeddingRows) {
|
|
8178
|
+
if (row.mediaKey !== void 0) embeddingMediaKeyByTrackId.set(row.trackId, row.mediaKey);
|
|
8179
|
+
if (row.keyFrameMediaKey !== void 0) keyFrameKeyByTrackId.set(row.trackId, row.keyFrameMediaKey);
|
|
8180
|
+
}
|
|
7228
8181
|
const encoderInfo = await api.embeddingEncoder.getInfo.query().catch((err) => {
|
|
7229
8182
|
this.ctx.logger.warn("searchObjectEvents: getInfo failed, proceeding without modelId gate", { meta: { error: String(err) } });
|
|
7230
8183
|
return null;
|
|
@@ -7271,10 +8224,20 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
7271
8224
|
score
|
|
7272
8225
|
});
|
|
7273
8226
|
}
|
|
7274
|
-
for (const { event, score } of bestEventByTrackId.values())
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
8227
|
+
for (const { event, score } of bestEventByTrackId.values()) {
|
|
8228
|
+
const mediaUrl = resolveSearchThumbnailUrl({
|
|
8229
|
+
baseUrl: this.eventMediaBaseUrl,
|
|
8230
|
+
eventId: event.id,
|
|
8231
|
+
...event.trackId !== void 0 && embeddingMediaKeyByTrackId.has(event.trackId) ? { embeddingMediaKey: embeddingMediaKeyByTrackId.get(event.trackId) } : {}
|
|
8232
|
+
});
|
|
8233
|
+
const keyFrameMediaKey = event.trackId !== void 0 ? keyFrameKeyByTrackId.get(event.trackId) : void 0;
|
|
8234
|
+
scored.push({
|
|
8235
|
+
...event,
|
|
8236
|
+
score,
|
|
8237
|
+
...mediaUrl !== void 0 ? { mediaUrl } : {},
|
|
8238
|
+
...keyFrameMediaKey !== void 0 ? { keyFrameMediaKey } : {}
|
|
8239
|
+
});
|
|
8240
|
+
}
|
|
7278
8241
|
}
|
|
7279
8242
|
scored.sort((a, b) => b.score - a.score);
|
|
7280
8243
|
return scored.slice(0, input.limit);
|
|
@@ -7306,6 +8269,24 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
7306
8269
|
* (native-res boxed frame) → any available file. Returns `null` if the
|
|
7307
8270
|
* event has no media at all.
|
|
7308
8271
|
*/
|
|
8272
|
+
/**
|
|
8273
|
+
* Data-plane resolver: an id is EITHER a bare event id (a UUID → the event's
|
|
8274
|
+
* crop, today's behaviour) OR a MediaStore key (`ownerKind:ownerId:kind:ts`,
|
|
8275
|
+
* contains ':' → served directly by key). The object-embedding search hit
|
|
8276
|
+
* points its thumbnail at the embedding row's crop key (a `track:…:crop:…`
|
|
8277
|
+
* key), so this resolves that crop; event ids stay on the event-crop path.
|
|
8278
|
+
*/
|
|
8279
|
+
async readMediaByEventOrKey(id) {
|
|
8280
|
+
if (id.includes(":")) {
|
|
8281
|
+
const file = await (this.mediaStore?.getByKey(id) ?? Promise.resolve(null));
|
|
8282
|
+
if (!file) return null;
|
|
8283
|
+
return {
|
|
8284
|
+
bytes: Buffer.from(file.base64, "base64"),
|
|
8285
|
+
key: file.key
|
|
8286
|
+
};
|
|
8287
|
+
}
|
|
8288
|
+
return this.readEventThumbnail(id);
|
|
8289
|
+
}
|
|
7309
8290
|
async readEventThumbnail(eventId) {
|
|
7310
8291
|
const files = await (this.mediaStore?.listByOwner("event", eventId) ?? Promise.resolve([]));
|
|
7311
8292
|
const chosen = files.find((f) => f.kind === "crop") ?? files.find((f) => f.kind === "fullFrameBoxed") ?? files[0];
|
|
@@ -7394,7 +8375,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
7394
8375
|
min: 500,
|
|
7395
8376
|
max: 6e4,
|
|
7396
8377
|
step: 500,
|
|
7397
|
-
default:
|
|
8378
|
+
default: 5e3,
|
|
7398
8379
|
showValue: true,
|
|
7399
8380
|
unit: "s",
|
|
7400
8381
|
displayScale: 1e3
|