@camstack/addon-post-analysis 1.1.25 → 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-CCC79h7t.js → dist-BEx5ST1W.js} +143 -6
- package/dist/{dist-Csk_yJr_.mjs → dist-DytVmDZg.mjs} +138 -7
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/enrichment-engine/index.js +2 -2
- package/dist/enrichment-engine/index.mjs +1 -1
- package/dist/{node-CIEkEv1F.js → node-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-Cb6Z2H5K.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-BrqCuPYQ.mjs → hostInit-2UZIpU0W.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +763 -40
- package/dist/pipeline-analytics/index.mjs +761 -38
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/dist/{resolve-frame-sKYbstL-.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;
|
|
@@ -1569,6 +1713,115 @@ var BestDetectionTracker = class {
|
|
|
1569
1713
|
}
|
|
1570
1714
|
};
|
|
1571
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
|
|
1572
1825
|
//#region src/pipeline-analytics/pipeline/native-detection.ts
|
|
1573
1826
|
/**
|
|
1574
1827
|
* Nominal frame size used to denormalize native `[0,1]` boxes when a
|
|
@@ -1738,6 +1991,18 @@ var TRACKS_COLUMNS = [
|
|
|
1738
1991
|
{
|
|
1739
1992
|
name: "state",
|
|
1740
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"
|
|
1741
2006
|
}
|
|
1742
2007
|
];
|
|
1743
2008
|
var TRACKS_INDEXES = [{
|
|
@@ -1769,7 +2034,10 @@ function cloneTrack(t) {
|
|
|
1769
2034
|
zonesVisited: [...t.zonesVisited],
|
|
1770
2035
|
totalDistance: t.totalDistance,
|
|
1771
2036
|
state: t.state,
|
|
1772
|
-
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 } : {}
|
|
1773
2041
|
};
|
|
1774
2042
|
}
|
|
1775
2043
|
var TrackStore = class {
|
|
@@ -1903,6 +2171,37 @@ var TrackStore = class {
|
|
|
1903
2171
|
}
|
|
1904
2172
|
}
|
|
1905
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
|
+
/**
|
|
1906
2205
|
* Clear a track's label. Sets label to null in the persisted row (so
|
|
1907
2206
|
* rowToTrack's `typeof label === 'string'` guard omits it on read → label
|
|
1908
2207
|
* is absent/undefined). Also clears the in-memory active entry if present.
|
|
@@ -1973,7 +2272,10 @@ var TrackStore = class {
|
|
|
1973
2272
|
snapshots: [...t.snapshots],
|
|
1974
2273
|
zonesVisited: [...t.zonesVisited],
|
|
1975
2274
|
totalDistance: t.totalDistance,
|
|
1976
|
-
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 } : {}
|
|
1977
2279
|
}
|
|
1978
2280
|
});
|
|
1979
2281
|
}
|
|
@@ -1982,6 +2284,9 @@ var TrackStore = class {
|
|
|
1982
2284
|
const snapshots = data["snapshots"] ?? [];
|
|
1983
2285
|
const zones = data["zonesVisited"] ?? [];
|
|
1984
2286
|
const label = data["label"];
|
|
2287
|
+
const importance = data["importance"];
|
|
2288
|
+
const bestEventId = data["bestEventId"];
|
|
2289
|
+
const importanceReason = data["importanceReason"];
|
|
1985
2290
|
return {
|
|
1986
2291
|
trackId: id,
|
|
1987
2292
|
deviceId: Number(data["deviceId"]),
|
|
@@ -1994,7 +2299,10 @@ var TrackStore = class {
|
|
|
1994
2299
|
zonesVisited: zones,
|
|
1995
2300
|
totalDistance: Number(data["totalDistance"] ?? 0),
|
|
1996
2301
|
state: data["state"] ?? "idle",
|
|
1997
|
-
active: false
|
|
2302
|
+
active: false,
|
|
2303
|
+
...typeof importance === "number" ? { importance } : {},
|
|
2304
|
+
...typeof bestEventId === "string" ? { bestEventId } : {},
|
|
2305
|
+
...typeof importanceReason === "string" ? { importanceReason } : {}
|
|
1998
2306
|
};
|
|
1999
2307
|
}
|
|
2000
2308
|
};
|
|
@@ -2497,6 +2805,10 @@ var OBJECT_COLUMNS = [
|
|
|
2497
2805
|
{
|
|
2498
2806
|
name: "mediaKey",
|
|
2499
2807
|
type: "TEXT"
|
|
2808
|
+
},
|
|
2809
|
+
{
|
|
2810
|
+
name: "importance",
|
|
2811
|
+
type: "REAL"
|
|
2500
2812
|
}
|
|
2501
2813
|
];
|
|
2502
2814
|
var AUDIO_COLUMNS = [
|
|
@@ -2694,6 +3006,63 @@ var EventStore = class {
|
|
|
2694
3006
|
return updated;
|
|
2695
3007
|
}
|
|
2696
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
|
+
/**
|
|
2697
3066
|
* Clear `label` on every already-emitted object event of a track (sets label
|
|
2698
3067
|
* to null so stripNulls/slimObject omit it on read → label is absent/undefined).
|
|
2699
3068
|
* Returns the number of events updated. Best-effort per row. Mirrors
|
|
@@ -2888,7 +3257,8 @@ function slimObject(id, data) {
|
|
|
2888
3257
|
timestamp: data["timestamp"],
|
|
2889
3258
|
className: data["className"],
|
|
2890
3259
|
...typeof data["frameId"] === "string" ? { frameId: data["frameId"] } : {},
|
|
2891
|
-
...typeof data["trackId"] === "string" ? { trackId: data["trackId"] } : {}
|
|
3260
|
+
...typeof data["trackId"] === "string" ? { trackId: data["trackId"] } : {},
|
|
3261
|
+
...typeof data["importance"] === "number" ? { importance: data["importance"] } : {}
|
|
2892
3262
|
};
|
|
2893
3263
|
if (typeof data["label"] === "string") return {
|
|
2894
3264
|
...base,
|
|
@@ -2918,6 +3288,17 @@ function slimAudio(id, data) {
|
|
|
2918
3288
|
}
|
|
2919
3289
|
return base;
|
|
2920
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
|
+
}
|
|
2921
3302
|
function stripNulls(data) {
|
|
2922
3303
|
const out = {};
|
|
2923
3304
|
for (const [k, v] of Object.entries(data)) if (v !== null) out[k] = v;
|
|
@@ -4537,6 +4918,14 @@ var FACE_COLUMNS = [
|
|
|
4537
4918
|
{
|
|
4538
4919
|
name: "assignedSampleId",
|
|
4539
4920
|
type: "TEXT"
|
|
4921
|
+
},
|
|
4922
|
+
{
|
|
4923
|
+
name: "keyFrameMediaKey",
|
|
4924
|
+
type: "TEXT"
|
|
4925
|
+
},
|
|
4926
|
+
{
|
|
4927
|
+
name: "faceBbox",
|
|
4928
|
+
type: "JSON"
|
|
4540
4929
|
}
|
|
4541
4930
|
];
|
|
4542
4931
|
var FACE_INDEXES = [{
|
|
@@ -4609,7 +4998,9 @@ var FaceStore = class {
|
|
|
4609
4998
|
assigned: Boolean(r.data.assigned),
|
|
4610
4999
|
mediaKey: data.mediaKey ?? void 0,
|
|
4611
5000
|
recognizedIdentityId: data.recognizedIdentityId ?? void 0,
|
|
4612
|
-
assignedSampleId: data.assignedSampleId ?? void 0
|
|
5001
|
+
assignedSampleId: data.assignedSampleId ?? void 0,
|
|
5002
|
+
keyFrameMediaKey: data.keyFrameMediaKey ?? void 0,
|
|
5003
|
+
faceBbox: data.faceBbox ?? void 0
|
|
4613
5004
|
};
|
|
4614
5005
|
}).filter((f) => !f.assigned);
|
|
4615
5006
|
}
|
|
@@ -4773,8 +5164,11 @@ var FaceStore = class {
|
|
|
4773
5164
|
id: faceId,
|
|
4774
5165
|
...data,
|
|
4775
5166
|
assigned: Boolean(raw.assigned),
|
|
5167
|
+
mediaKey: data.mediaKey ?? void 0,
|
|
4776
5168
|
recognizedIdentityId: data.recognizedIdentityId ?? void 0,
|
|
4777
|
-
assignedSampleId: data.assignedSampleId ?? void 0
|
|
5169
|
+
assignedSampleId: data.assignedSampleId ?? void 0,
|
|
5170
|
+
keyFrameMediaKey: data.keyFrameMediaKey ?? void 0,
|
|
5171
|
+
faceBbox: data.faceBbox ?? void 0
|
|
4778
5172
|
};
|
|
4779
5173
|
}
|
|
4780
5174
|
/**
|
|
@@ -4818,7 +5212,9 @@ var FaceStore = class {
|
|
|
4818
5212
|
assigned: Boolean(r.data.assigned),
|
|
4819
5213
|
mediaKey: data.mediaKey ?? void 0,
|
|
4820
5214
|
recognizedIdentityId: data.recognizedIdentityId ?? void 0,
|
|
4821
|
-
assignedSampleId: data.assignedSampleId ?? void 0
|
|
5215
|
+
assignedSampleId: data.assignedSampleId ?? void 0,
|
|
5216
|
+
keyFrameMediaKey: data.keyFrameMediaKey ?? void 0,
|
|
5217
|
+
faceBbox: data.faceBbox ?? void 0
|
|
4822
5218
|
};
|
|
4823
5219
|
});
|
|
4824
5220
|
const filterMode = input.filter ?? "all";
|
|
@@ -4883,6 +5279,10 @@ var OBJECT_EMBEDDING_COLUMNS = [
|
|
|
4883
5279
|
{
|
|
4884
5280
|
name: "mediaKey",
|
|
4885
5281
|
type: "TEXT"
|
|
5282
|
+
},
|
|
5283
|
+
{
|
|
5284
|
+
name: "keyFrameMediaKey",
|
|
5285
|
+
type: "TEXT"
|
|
4886
5286
|
}
|
|
4887
5287
|
];
|
|
4888
5288
|
var ObjectEmbeddingStore = class {
|
|
@@ -4929,7 +5329,8 @@ var ObjectEmbeddingStore = class {
|
|
|
4929
5329
|
modelId: input.modelId,
|
|
4930
5330
|
dim: input.embedding.length,
|
|
4931
5331
|
confidence: input.confidence,
|
|
4932
|
-
...input.mediaKey !== void 0 ? { mediaKey: input.mediaKey } : {}
|
|
5332
|
+
...input.mediaKey !== void 0 ? { mediaKey: input.mediaKey } : {},
|
|
5333
|
+
...input.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: input.keyFrameMediaKey } : {}
|
|
4933
5334
|
};
|
|
4934
5335
|
try {
|
|
4935
5336
|
await this.store.set.mutate({
|
|
@@ -4967,7 +5368,8 @@ var ObjectEmbeddingStore = class {
|
|
|
4967
5368
|
return {
|
|
4968
5369
|
id: r.id,
|
|
4969
5370
|
...data,
|
|
4970
|
-
mediaKey: data.mediaKey ?? void 0
|
|
5371
|
+
mediaKey: data.mediaKey ?? void 0,
|
|
5372
|
+
keyFrameMediaKey: data.keyFrameMediaKey ?? void 0
|
|
4971
5373
|
};
|
|
4972
5374
|
});
|
|
4973
5375
|
}
|
|
@@ -5096,6 +5498,8 @@ function updateTrackAggregate(prev, match, opts) {
|
|
|
5096
5498
|
}
|
|
5097
5499
|
//#endregion
|
|
5098
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;
|
|
5099
5503
|
var FaceRecognizer = class {
|
|
5100
5504
|
deps;
|
|
5101
5505
|
gallery = [];
|
|
@@ -5107,6 +5511,9 @@ var FaceRecognizer = class {
|
|
|
5107
5511
|
* true highest-confidence face (holding a buffer in memory is cheap, and a
|
|
5108
5512
|
* track may last well under the best-frame rate-limit window). */
|
|
5109
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;
|
|
5110
5517
|
constructor(deps) {
|
|
5111
5518
|
this.deps = deps;
|
|
5112
5519
|
}
|
|
@@ -5236,6 +5643,17 @@ var FaceRecognizer = class {
|
|
|
5236
5643
|
}
|
|
5237
5644
|
});
|
|
5238
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
|
+
}
|
|
5239
5657
|
}
|
|
5240
5658
|
}
|
|
5241
5659
|
/**
|
|
@@ -5255,9 +5673,23 @@ var FaceRecognizer = class {
|
|
|
5255
5673
|
} });
|
|
5256
5674
|
return;
|
|
5257
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
|
+
}
|
|
5258
5690
|
const faceId = `face-${trackId}`;
|
|
5259
5691
|
let mediaKey;
|
|
5260
|
-
|
|
5692
|
+
try {
|
|
5261
5693
|
mediaKey = await this.deps.mediaStore.put({
|
|
5262
5694
|
deviceId,
|
|
5263
5695
|
ownerKind: "face",
|
|
@@ -5275,6 +5707,17 @@ var FaceRecognizer = class {
|
|
|
5275
5707
|
}
|
|
5276
5708
|
});
|
|
5277
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);
|
|
5278
5721
|
try {
|
|
5279
5722
|
await this.deps.faceStore.insert({
|
|
5280
5723
|
id: faceId,
|
|
@@ -5282,9 +5725,11 @@ var FaceRecognizer = class {
|
|
|
5282
5725
|
trackId,
|
|
5283
5726
|
timestamp: held.timestamp,
|
|
5284
5727
|
embedding: held.embedding,
|
|
5285
|
-
|
|
5728
|
+
mediaKey,
|
|
5286
5729
|
...held.recognizedIdentityId !== void 0 ? { recognizedIdentityId: held.recognizedIdentityId } : {},
|
|
5287
|
-
assigned: false
|
|
5730
|
+
assigned: false,
|
|
5731
|
+
faceBbox: held.bbox,
|
|
5732
|
+
...keyFrameMediaKey !== void 0 ? { keyFrameMediaKey } : {}
|
|
5288
5733
|
});
|
|
5289
5734
|
this.deps.logger.info("face: buffered to gallery", {
|
|
5290
5735
|
tags: {
|
|
@@ -5293,7 +5738,7 @@ var FaceRecognizer = class {
|
|
|
5293
5738
|
},
|
|
5294
5739
|
meta: {
|
|
5295
5740
|
faceId,
|
|
5296
|
-
hasCrop:
|
|
5741
|
+
hasCrop: true,
|
|
5297
5742
|
recognizedIdentityId: held.recognizedIdentityId ?? null,
|
|
5298
5743
|
score: held.score
|
|
5299
5744
|
}
|
|
@@ -5625,6 +6070,38 @@ var PlateRecognizer = class {
|
|
|
5625
6070
|
}
|
|
5626
6071
|
};
|
|
5627
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
|
|
5628
6105
|
//#region src/pipeline-analytics/pipeline/event-child-crops.ts
|
|
5629
6106
|
/**
|
|
5630
6107
|
* Compute Intersection-over-Union between two (x,y,w,h) bounding boxes.
|
|
@@ -5929,6 +6406,18 @@ var SETTINGS_CACHE_TTL_MS = 5e3;
|
|
|
5929
6406
|
var BEST_FRAME_HYSTERESIS = .05;
|
|
5930
6407
|
/** §5 best-frame: at most one best-thumbnail capture per this interval per track. */
|
|
5931
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;
|
|
5932
6421
|
/** Cluster setting key (in the centralized addon store) selecting the SINGLE
|
|
5933
6422
|
* node that runs post-analysis (event/media/audio/motion generation). All
|
|
5934
6423
|
* other nodes are fully inert. No multi-node balancing. Default: the hub. */
|
|
@@ -6048,6 +6537,23 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6048
6537
|
hysteresis: BEST_FRAME_HYSTERESIS,
|
|
6049
6538
|
minGapMs: BEST_FRAME_MIN_GAP_MS
|
|
6050
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;
|
|
6051
6557
|
shuttingDown = false;
|
|
6052
6558
|
/** True only on the cluster's designated post-processing node. When false the
|
|
6053
6559
|
* addon subscribes to NOTHING — fully inert (no event/media generation). */
|
|
@@ -6081,7 +6587,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6081
6587
|
let storage = this.ctx.kernel.storage;
|
|
6082
6588
|
const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
|
|
6083
6589
|
if (mediaRoot) {
|
|
6084
|
-
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-
|
|
6590
|
+
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-Cg_cGqs0.js"));
|
|
6085
6591
|
storage = new FilesystemStorageProvider(mediaRoot);
|
|
6086
6592
|
logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
|
|
6087
6593
|
}
|
|
@@ -6147,23 +6653,62 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6147
6653
|
});
|
|
6148
6654
|
const ownNodeIdForFaces = ownNodeId;
|
|
6149
6655
|
const frameReadersForFaces = this.frameReaders;
|
|
6150
|
-
const
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
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) => {
|
|
6158
6694
|
const paddedNorm = padBbox({
|
|
6159
6695
|
x: bbox.x / frameWidth,
|
|
6160
6696
|
y: bbox.y / frameHeight,
|
|
6161
6697
|
w: bbox.w / frameWidth,
|
|
6162
6698
|
h: bbox.h / frameHeight
|
|
6163
6699
|
}, padding);
|
|
6164
|
-
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);
|
|
6165
6709
|
return crop;
|
|
6166
6710
|
};
|
|
6711
|
+
this.captureCrop = captureCrop;
|
|
6167
6712
|
this.faceRecognizer = new FaceRecognizer({
|
|
6168
6713
|
identityStore: this.identityStore,
|
|
6169
6714
|
faceStore: this.faceStore,
|
|
@@ -6171,6 +6716,16 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6171
6716
|
trackStore: this.trackStore,
|
|
6172
6717
|
eventStore: this.eventStore,
|
|
6173
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),
|
|
6174
6729
|
logger: logger.child("FaceRecognizer")
|
|
6175
6730
|
});
|
|
6176
6731
|
this.faceRecognizer.refreshGallery();
|
|
@@ -6208,7 +6763,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6208
6763
|
try {
|
|
6209
6764
|
const handler = createEventMediaHandler({ getMedia: async (id) => {
|
|
6210
6765
|
try {
|
|
6211
|
-
return await this.
|
|
6766
|
+
return await this.readMediaByEventOrKey(id);
|
|
6212
6767
|
} catch (err) {
|
|
6213
6768
|
this.ctx.logger.warn("readEventThumbnail failed", { meta: {
|
|
6214
6769
|
eventId: id,
|
|
@@ -6592,6 +7147,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6592
7147
|
this.lastActiveTrackIds.clear();
|
|
6593
7148
|
this.dropoutSkipsByKey.clear();
|
|
6594
7149
|
this.bestFrameTracker.clear();
|
|
7150
|
+
this.objectEmbeddingBestSelector.clear();
|
|
6595
7151
|
this.levelStateByDevice.clear();
|
|
6596
7152
|
this.settingsCacheByDevice.clear();
|
|
6597
7153
|
this.sensitivityCacheByDevice.clear();
|
|
@@ -6747,16 +7303,18 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6747
7303
|
} });
|
|
6748
7304
|
}
|
|
6749
7305
|
await Promise.all(result.objectEvents.map((e) => this.eventStore.insertObject(e)));
|
|
6750
|
-
|
|
6751
|
-
|
|
7306
|
+
const objectEmbeddingBests = [];
|
|
7307
|
+
if (this.objectEmbeddingStore) for (const t of result.tracked) {
|
|
7308
|
+
if (!isClipObjectEmbedding(t)) continue;
|
|
7309
|
+
if (this.objectEmbeddingBestSelector.observe({
|
|
6752
7310
|
trackId: t.trackId,
|
|
6753
|
-
|
|
6754
|
-
|
|
7311
|
+
confidence: t.confidence,
|
|
7312
|
+
atMs: result.timestamp,
|
|
6755
7313
|
className: t.className,
|
|
7314
|
+
bbox: t.bbox,
|
|
6756
7315
|
embedding: t.embedding,
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
});
|
|
7316
|
+
embeddingModelId: t.embeddingModelId
|
|
7317
|
+
})) objectEmbeddingBests.push(t);
|
|
6760
7318
|
}
|
|
6761
7319
|
const faceSettings = this.faceRecognizer ? await this.resolveDeviceFaceSettings(deviceId) : null;
|
|
6762
7320
|
const faceGloballyEnabled = this.faceRecognizer ? await this.resolveGlobalFaceEnabled() : false;
|
|
@@ -6829,6 +7387,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6829
7387
|
cropPadding: mediaSettings.cropPadding,
|
|
6830
7388
|
...frameHandle !== void 0 ? { frameHandle } : {}
|
|
6831
7389
|
});
|
|
7390
|
+
if (objectEmbeddingBests.length > 0) this.persistObjectEmbeddingBests(deviceId, result.timestamp, objectEmbeddingBests, frameHandle, result.frameWidth, result.frameHeight, mediaSettings.cropPadding);
|
|
6832
7391
|
for (const e of result.objectEvents) this.ctx.eventBus.emit({
|
|
6833
7392
|
id: `pa-${e.id}`,
|
|
6834
7393
|
timestamp: new Date(e.timestamp),
|
|
@@ -6950,6 +7509,83 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6950
7509
|
* NOT gated by saveThumbnails (a single best still-frame is always useful).
|
|
6951
7510
|
* The per-track best-confidence map is updated here as a side effect.
|
|
6952
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
|
+
}
|
|
6953
7589
|
buildSnapshotTargets(tracked, timestamp, media) {
|
|
6954
7590
|
const targets = [];
|
|
6955
7591
|
for (const t of tracked) {
|
|
@@ -7221,9 +7857,36 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
7221
7857
|
positions: t.positions.length
|
|
7222
7858
|
}
|
|
7223
7859
|
});
|
|
7224
|
-
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();
|
|
7225
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
|
+
}
|
|
7226
7888
|
this.bestFrameTracker.delete(t.trackId);
|
|
7889
|
+
this.objectEmbeddingBestSelector.delete(t.trackId);
|
|
7227
7890
|
this.ctx.eventBus.emit({
|
|
7228
7891
|
id: `pa-end-${t.trackId}`,
|
|
7229
7892
|
timestamp: new Date(t.lastSeen),
|
|
@@ -7446,6 +8109,32 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
7446
8109
|
if (input.projection !== "slim" || this.eventMediaBaseUrl === null) return events;
|
|
7447
8110
|
return this.withMediaUrl(events);
|
|
7448
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
|
+
}
|
|
7449
8138
|
async getAudioEvents(input) {
|
|
7450
8139
|
const events = await (this.eventStore?.queryAudio(input) ?? Promise.resolve([]));
|
|
7451
8140
|
if (input.projection !== "slim" || this.eventMediaBaseUrl === null) return events;
|
|
@@ -7488,6 +8177,12 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
7488
8177
|
className: input.classFilter
|
|
7489
8178
|
});
|
|
7490
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
|
+
}
|
|
7491
8186
|
const encoderInfo = await api.embeddingEncoder.getInfo.query().catch((err) => {
|
|
7492
8187
|
this.ctx.logger.warn("searchObjectEvents: getInfo failed, proceeding without modelId gate", { meta: { error: String(err) } });
|
|
7493
8188
|
return null;
|
|
@@ -7534,10 +8229,20 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
7534
8229
|
score
|
|
7535
8230
|
});
|
|
7536
8231
|
}
|
|
7537
|
-
for (const { event, score } of bestEventByTrackId.values())
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
|
|
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
|
+
}
|
|
7541
8246
|
}
|
|
7542
8247
|
scored.sort((a, b) => b.score - a.score);
|
|
7543
8248
|
return scored.slice(0, input.limit);
|
|
@@ -7569,6 +8274,24 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
7569
8274
|
* (native-res boxed frame) → any available file. Returns `null` if the
|
|
7570
8275
|
* event has no media at all.
|
|
7571
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
|
+
}
|
|
7572
8295
|
async readEventThumbnail(eventId) {
|
|
7573
8296
|
const files = await (this.mediaStore?.listByOwner("event", eventId) ?? Promise.resolve([]));
|
|
7574
8297
|
const chosen = files.find((f) => f.kind === "crop") ?? files.find((f) => f.kind === "fullFrameBoxed") ?? files[0];
|