@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.
@@ -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-CCC79h7t.js");
6
- const require_resolve_frame = require("../resolve-frame-sKYbstL-.js");
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
- if (held.crop !== void 0) try {
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
- ...mediaKey !== void 0 ? { mediaKey } : {},
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: mediaKey !== void 0,
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-CIEkEv1F.js"));
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 captureCrop = async (frameHandle, bbox, frameWidth, frameHeight, padding) => {
6151
- const decoded = await require_resolve_frame.resolveFrame(frameHandle, {
6152
- ownNodeId: ownNodeIdForFaces,
6153
- readers: frameReadersForFaces,
6154
- getRemoteFrame
6155
- });
6156
- if (!decoded || decoded.format !== "rgb") return null;
6157
- const data = Buffer.isBuffer(decoded.data) ? decoded.data : Buffer.from(decoded.data);
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 { crop } = await require_resolve_frame.extractCrop(data, decoded.width, decoded.height, paddedNorm);
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.readEventThumbnail(id);
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
- if (this.objectEmbeddingStore) {
6751
- for (const t of result.tracked) if (t.embedding !== void 0 && t.embeddingModelId !== void 0 && t.embeddingModelId.startsWith("mobileclip-")) this.objectEmbeddingStore.upsertIfBetter({
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
- deviceId,
6754
- timestamp: result.timestamp,
7311
+ confidence: t.confidence,
7312
+ atMs: result.timestamp,
6755
7313
  className: t.className,
7314
+ bbox: t.bbox,
6756
7315
  embedding: t.embedding,
6757
- modelId: t.embeddingModelId,
6758
- confidence: t.confidence
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()) scored.push({
7538
- ...event,
7539
- score
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];